From bca07080f0ff8c9a1b6ba310f4570bde7da5a9da Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:56:14 +0200 Subject: [PATCH] change everything ^^ --- .claude/settings.local.json | 4 +- PERMISSIONED_DATA.md | 140 + README.md | 73 +- app/config.ts | 14 +- examples/cloudflare-workers/README.md | 10 +- examples/cloudflare-workers/config.ts | 10 +- examples/postgres/README.md | 10 +- examples/postgres/config.ts | 10 +- .../sveltekit-cloudflare-workers/README.md | 5 +- .../statusphere/app/getProfile.json | 17 +- .../statusphere/app/permissionSet.json | 26 + .../app}/status/getRecord.json | 2 +- .../app}/status/listRecords.json | 2 +- .../sveltekit-cloudflare-workers/package.json | 6 +- .../src/lib/contrail/config.ts | 3 +- .../src/routes/+page.server.ts | 2 +- .../atmo}/event/getRecord.json | 17 +- .../atmo}/event/listRecords.json | 22 +- .../rsvp/atmo/permissionSet.json | 42 + .../atmo}/rsvp/getRecord.json | 17 +- .../atmo}/rsvp/listRecords.json | 22 +- .../rsvp/atmo/space/admin/addMember.json | 60 + .../rsvp/atmo/space/admin/createSpace.json | 59 + .../rsvp/atmo/space/admin/removeMember.json | 55 + lexicons-generated/rsvp/atmo/space/defs.json | 193 + .../rsvp/atmo/space/deleteRecord.json | 56 + .../rsvp/atmo/space/getRecord.json | 59 + .../rsvp/atmo/space/getSpace.json | 45 + .../rsvp/atmo/space/invite/create.json | 74 + .../rsvp/atmo/space/invite/list.json | 52 + .../rsvp/atmo/space/invite/redeem.json | 52 + .../rsvp/atmo/space/invite/revoke.json | 51 + .../rsvp/atmo/space/listMembers.json | 48 + .../rsvp/atmo/space/listRecords.json | 70 + .../rsvp/atmo/space/listSpaces.json | 57 + .../rsvp/atmo/space/putRecord.json | 68 + lexicons/README.md | 16 + package.json | 13 +- pnpm-lock.yaml | 3506 ++++++++++++++++- scripts/publish-lexicons.ts | 42 + .../admin/addMember.json | 2 +- .../admin/createSpace.json | 1 - .../admin/removeMember.json | 36 + .../defs.json | 13 +- spaces-lexicon-templates/deleteRecord.json | 34 + .../getRecord.json | 0 .../getSpace.json | 0 .../invite/create.json | 2 +- .../invite/list.json | 0 .../invite/redeem.json | 2 +- .../invite/revoke.json | 0 spaces-lexicon-templates/listMembers.json | 34 + .../listRecords.json | 0 spaces-lexicon-templates/listSpaces.json | 33 + .../putRecord.json | 0 src/core/backfill.ts | 6 +- src/core/db/index.ts | 2 +- src/core/db/records.ts | 298 +- src/core/db/schema.ts | 161 +- src/core/jetstream.ts | 10 +- src/core/persistent.ts | 6 +- src/core/router/admin.ts | 9 +- src/core/router/collection.ts | 195 +- src/core/router/helpers.ts | 1 + src/core/router/hydrate.ts | 202 +- src/core/router/index.ts | 28 +- src/core/router/notify.ts | 7 +- src/core/router/profiles.ts | 8 +- src/core/spaces/acl.ts | 91 +- src/core/spaces/adapter.ts | 275 +- src/core/spaces/auth.ts | 54 +- src/core/spaces/router.ts | 142 +- src/core/spaces/schema.ts | 54 +- src/core/spaces/types.ts | 28 +- src/core/types.ts | 243 +- src/generate.ts | 285 +- src/index.ts | 4 +- src/lexicons.ts | 15 - src/publish.ts | 205 + tests/generate.test.ts | 105 +- tests/helpers.ts | 26 +- tests/hydrate.test.ts | 46 +- tests/notify.test.ts | 18 +- tests/records.test.ts | 6 +- tests/schema.test.ts | 4 +- tests/spaces-acl.test.ts | 214 +- tests/spaces-e2e.test.ts | 52 +- tests/spaces-invites.test.ts | 45 +- tests/types.test.ts | 134 +- tsup.config.ts | 2 +- 90 files changed, 7213 insertions(+), 955 deletions(-) create mode 100644 PERMISSIONED_DATA.md create mode 100644 examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/permissionSet.json rename examples/sveltekit-cloudflare-workers/lexicons-generated/{xyz/statusphere => statusphere/app}/status/getRecord.json (98%) rename examples/sveltekit-cloudflare-workers/lexicons-generated/{xyz/statusphere => statusphere/app}/status/listRecords.json (99%) rename lexicons-generated/{community/lexicon/calendar => rsvp/atmo}/event/getRecord.json (92%) rename lexicons-generated/{community/lexicon/calendar => rsvp/atmo}/event/listRecords.json (93%) create mode 100644 lexicons-generated/rsvp/atmo/permissionSet.json rename lexicons-generated/{community/lexicon/calendar => rsvp/atmo}/rsvp/getRecord.json (91%) rename lexicons-generated/{community/lexicon/calendar => rsvp/atmo}/rsvp/listRecords.json (90%) create mode 100644 lexicons-generated/rsvp/atmo/space/admin/addMember.json create mode 100644 lexicons-generated/rsvp/atmo/space/admin/createSpace.json create mode 100644 lexicons-generated/rsvp/atmo/space/admin/removeMember.json create mode 100644 lexicons-generated/rsvp/atmo/space/defs.json create mode 100644 lexicons-generated/rsvp/atmo/space/deleteRecord.json create mode 100644 lexicons-generated/rsvp/atmo/space/getRecord.json create mode 100644 lexicons-generated/rsvp/atmo/space/getSpace.json create mode 100644 lexicons-generated/rsvp/atmo/space/invite/create.json create mode 100644 lexicons-generated/rsvp/atmo/space/invite/list.json create mode 100644 lexicons-generated/rsvp/atmo/space/invite/redeem.json create mode 100644 lexicons-generated/rsvp/atmo/space/invite/revoke.json create mode 100644 lexicons-generated/rsvp/atmo/space/listMembers.json create mode 100644 lexicons-generated/rsvp/atmo/space/listRecords.json create mode 100644 lexicons-generated/rsvp/atmo/space/listSpaces.json create mode 100644 lexicons-generated/rsvp/atmo/space/putRecord.json create mode 100644 lexicons/README.md create mode 100644 scripts/publish-lexicons.ts rename {lexicons/tools/atmo/space => spaces-lexicon-templates}/admin/addMember.json (89%) rename {lexicons/tools/atmo/space => spaces-lexicon-templates}/admin/createSpace.json (90%) create mode 100644 spaces-lexicon-templates/admin/removeMember.json rename {lexicons/tools/atmo/space => spaces-lexicon-templates}/defs.json (84%) create mode 100644 spaces-lexicon-templates/deleteRecord.json rename {lexicons/tools/atmo/space => spaces-lexicon-templates}/getRecord.json (100%) rename {lexicons/tools/atmo/space => spaces-lexicon-templates}/getSpace.json (100%) rename {lexicons/tools/atmo/space => spaces-lexicon-templates}/invite/create.json (92%) rename {lexicons/tools/atmo/space => spaces-lexicon-templates}/invite/list.json (100%) rename {lexicons/tools/atmo/space => spaces-lexicon-templates}/invite/redeem.json (91%) rename {lexicons/tools/atmo/space => spaces-lexicon-templates}/invite/revoke.json (100%) create mode 100644 spaces-lexicon-templates/listMembers.json rename {lexicons/tools/atmo/space => spaces-lexicon-templates}/listRecords.json (100%) create mode 100644 spaces-lexicon-templates/listSpaces.json rename {lexicons/tools/atmo/space => spaces-lexicon-templates}/putRecord.json (100%) delete mode 100644 src/lexicons.ts create mode 100644 src/publish.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ef80553..a3668cd 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,9 @@ "Bash(npx tsc:*)", "Bash(pnpm test:*)", "Bash(npx vitest:*)", - "Bash(npm test:*)" + "Bash(npm test:*)", + "Bash(npx lex-cli:*)", + "Bash(pnpm vitest:*)" ] } } diff --git a/PERMISSIONED_DATA.md b/PERMISSIONED_DATA.md new file mode 100644 index 0000000..ca25eec --- /dev/null +++ b/PERMISSIONED_DATA.md @@ -0,0 +1,140 @@ +# Permissioned Data + +Contrail ships an opt-in permissioned-spaces subsystem: a write-capable, auth-gated XRPC store for records that can't live on public PDSes. It's designed to be used today and to migrate cleanly to Bluesky's real [permissioned spaces](https://dholms.leaflet.pub/3mhj6bcqats2o) when those ship. + +## Why this exists + +Real atproto permissioned repos don't exist yet. Apps that need private data (invite-only events, team forums, group messaging) either go fully public and gate at the app layer (insecure), or roll their own server (locks data out of atproto entirely). + +Contrail's spaces feature is the middle path: a **centralized, auth-gated store** with the same primitives Bluesky proposed — spaces, member lists, app policies, service-auth tokens — so the data model, XRPC surface, and consent UX match what the protocol will eventually standardize. On migration day, you move data from our hosted DB into user PDSes; the API your app talks to doesn't change. + +**Tradeoff:** data lives in *your* database, not on user PDSes. Fine for interim use when you (or the space owner) run the service. Not a long-term replacement for real permissioned repos. + +## Mental model + +> A **space** is a bag of records with one lock. The **member list** says who has the key. + +- Each space has one owner (DID), one type (classifying NSID), one key (string). Identified by an at-uri: `at:////`. +- A space holds records of *any* NSID. Same role a PDS repo plays, scoped to a shared context. +- Each member has a **perms** value: `"read"` or `"write"`. The space owner is implicit `write`. +- Each space has an optional **app policy**: `{ mode: "allow" | "deny", apps: [client_id, …] }` — gates which OAuth clients can act in the space. + +That's it. Every feature (private channels, invite-only threads, shared albums) is one-space-per-permission-boundary with records of whatever NSIDs the app defines. If you need finer-grained access (e.g. "admins" vs "members"), model it as multiple spaces or enforce at the app layer. + +## Design choices worth naming + +**Each permission boundary = its own space.** No nested ACLs, no per-record permissions, no per-collection policies. Matches the blog's model; keeps the library generic. + +**Read/write as the only permission axis.** The library enforces: read = any member, write = members with `"write"` (owner always). Apps that need richer roles layer them on top of multiple spaces or check `clientId` / authorDid in their handlers. + +**atproto service-auth JWTs.** Verification uses `@atcute/xrpc-server/auth.ServiceJwtVerifier`: validates signature against issuer's DID doc, checks `aud` matches the configured service DID, checks `lxm` covers the method. Same path third-party apps and our own code go through. + +**Per-deployment namespace, not shared transport lexicons.** A deployment owning `com.example` emits `com.example.space.*` XRPCs and a `com.example.permissionSet` — OAuth consent screens show your domain, not a third party's. Client generation pulls lexicons from your repo. + +**Per-collection spaces tables with full parity.** Each collection you declare gets both `records_` (public) and `spaces_records_` (private), with the same count columns and indexes. Filters, sorts, hydration, and references all work in either mode. + +**Unified `listRecords`.** The per-collection endpoint accepts three call shapes: + +| Call | Returns | +| --- | --- | +| No auth, no `spaceUri` | Public records only | +| `?spaceUri=…` + service-auth JWT | Records from that one space (ACL-gated) | +| Service-auth JWT, no `spaceUri` | Public records **unioned** with records from every space the caller is a member of | + +The union path runs the public and per-space queries in parallel and merges with a shared keyset cursor, so filters, sorts, hydration, and references all work across sources. Records from a space carry a `space: ` field in the response. + +**Opaque keyset cursors.** `base64url(JSON({ t, v?, k }))` — tiebreaker `time_us`, sort-key value, and sort-kind tag. Cursors with a `k` that doesn't match the current sort are silently ignored (instead of returning wrong results). This lets one cursor be shared across every sub-query in the union. + +**Invites are stored tokens.** Random 32-byte token returned once on creation; SHA-256 hash is persisted. Redemption is a single atomic UPDATE that increments `used_count` only if `!revoked && !expired && !exhausted`. + +**Two-DB split is opt-in.** `initSchema(db, config, { spacesDb })` and `createApp(db, config, { spacesDb })`. Spaces tables can live on a separate binding for different backup/compliance constraints; defaults to the main DB. + +**Everything is opt-in.** No `config.spaces` set = zero extra tables, zero extra routes, zero new deps activated. + +## Architecture + +``` +src/core/spaces/ + types.ts — SpacesConfig, StorageAdapter, row types + schema.ts — DDL for spaces / spaces_members / spaces_invites base + per-collection tables + adapter.ts — HostedAdapter: CRUD + space-scoped count maintenance on putRecord/deleteRecord + acl.ts — pure checkAccess(): owner / member-read / member-write / app-policy logic + auth.ts — buildVerifier + verifyServiceAuthRequest + router.ts — Hono route registration for .space.* + invite-token.ts, tid.ts — crypto helpers +``` + +Auth flow (per request): + +1. **`.space.*` routes**: service-auth middleware validates the JWT (signature, `aud`, `lxm`) and populates `c.var.serviceAuth = { issuer, audience, lxm, clientId }`. Handler fetches space + caller's membership via the adapter, runs `checkAccess`, dispatches to the adapter on allow, returns 403 with a structured `reason` on deny. +2. **Per-collection `listRecords`/`getRecord` with `?spaceUri=…`**: same verify + ACL gate, then dispatches to the adapter (scoped to the one space). +3. **Per-collection `listRecords` with auth but no `spaceUri`**: verify, list caller's member spaces via `adapter.listSpaces({ memberDid })`, run the public + per-space union. + +## Usage + +```ts +import type { ContrailConfig } from "@atmo-dev/contrail"; + +const config: ContrailConfig = { + namespace: "myapp", + collections: { + event: { collection: "community.lexicon.calendar.event", /* ... */ }, + location: { collection: "myapp.event.location" }, + message: { collection: "myapp.event.message" }, + }, + spaces: { + type: "myapp.event.space", // classifying NSID for this kind of space + serviceDid: "did:web:myapp.com", // plain DID, no fragment — see note below + // resolver: optional — defaults to composite did:plc + did:web + // defaultAppPolicy: optional — app-level deny/allow list + }, +}; +``` + +Per-collection opt-out: + +```ts +collections: { + public_only: { collection: "myapp.public", allowInSpaces: false }, +} +``` + +Client-side (from another app consuming a space service): + +```ts +const response = await userPdsClient.post("myapp.space.putRecord", { + headers: { "Atproto-Proxy": "did:web:myapp.com#myapp_space" }, + input: { spaceUri, collection: "myapp.event.message", record: { text: "hi" } }, +}); +``` + +The user's PDS validates OAuth scope against `myapp.permissionSet` (auto-generated), mints a service-auth JWT with `aud=did:web:myapp.com`, forwards to your service, which verifies and executes. + +> **Note on service DIDs.** Use the plain DID (no service fragment) as `serviceDid`. Many PDS implementations reject `aud` values with `#fragment` in `com.atproto.server.getServiceAuth`, and the middleware does strict string equality on `aud`. The fragment form (`did:web:myapp.com#myapp_space`) only belongs in your DID doc's service entry, where PDSes use it to resolve the service endpoint URL for `Atproto-Proxy` routing — that's a separate concern from JWT audience validation. + +## DNS requirements + +Permission sets live under your namespace (`myapp.permissionSet`), which means PDSes need to resolve that NSID via DNS. atproto NSID resolution does **not** walk up subdomains — each emitted lexicon needs its own `TXT` record at `_lexicon.`. The publish script (`scripts/publish-lexicons.ts`) prints the exact records you need. You must control the domain. + +## What's deliberately not in here + +- **No E2EE.** Data is operator-readable. Appropriate for "invite-only" not "journalists-under-threat." +- **No per-space sharding/replication.** One DB, one operator. +- **No lexicon validation on write.** Add at the app layer if you need it. +- **No moderation/report primitives.** App layer. +- **No FTS in space mode (yet).** Search on `?spaceUri=…` is skipped. A composite-keyed `fts_spaces_` would fix this — flagged as follow-up because the same at-URI can appear in multiple spaces. +- **No hybrid single-record split.** A record is either on a public PDS (contrail-indexed) or inside a space. The unified `listRecords` stitches both sides back together at read time. + +## Migrating to real permissioned spaces + +Design choices made so migration is mostly data movement, not API redesign: + +- **TID rkeys from day one** — already valid atproto rkeys. +- **JSON payloads stored as they'd appear on a PDS** — no shape translation at migration time. +- **Per-member `authorDid` column** — each member can write their slice into their own permissioned repo via `applyWrites`. +- **`exportSpace` API planned** — will dump all records in the shape the PDS will accept. Migration day: loop over members, call export + applyWrites, flip a flag. Keep old DB read-only as fallback. +- **OAuth permission set follows the spec format** — one `permission` entry with `{ resource: "rpc", inheritAud: true, lxm: […] }`, so the same user consent works whether the target is your interim service DID or (eventually) a real permissioned-repo PDS. + +## Status + +Interim by design. If Bluesky ships real permissioned repos and you own the service, plan to migrate. If they don't, this shape is stable: we designed the transport and consent UX to match the protocol proposal, so the API your apps speak doesn't change on migration day. diff --git a/README.md b/README.md index 8589028..4e95bd6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ > [!WARNING] > Work in progress! Pre-alpha, expect breaking changes. -A library for indexing AT Protocol records. Define collections — get automatic Jetstream ingestion, PDS backfill, user discovery, and typed XRPC endpoints. Works with Cloudflare Workers + D1, SvelteKit, Node.js, or any JavaScript runtime. +A library for indexing AT Protocol records. Define collections — get automatic Jetstream ingestion, PDS backfill, user discovery, typed XRPC endpoints, and (opt-in) permissioned spaces for private records. Works with Cloudflare Workers + D1, SvelteKit, Node.js, or any JavaScript runtime. ## Install @@ -20,7 +20,8 @@ const contrail = new Contrail({ namespace: "com.example", db, // any Database-compatible instance (D1, SQLite, etc.) collections: { - "community.lexicon.calendar.event": { + event: { // short name → URL path + table suffix + collection: "community.lexicon.calendar.event", // full NSID of the record type queryable: { mode: {}, // string → equality filter (?mode=online) name: {}, // string → equality filter (?name=...) @@ -30,7 +31,7 @@ const contrail = new Contrail({ searchable: ["name", "description"], relations: { rsvps: { - collection: "community.lexicon.calendar.rsvp", + collection: "rsvp", // short name of the child collection groupBy: "status", count: true, groups: { @@ -41,14 +42,15 @@ const contrail = new Contrail({ }, }, }, - "community.lexicon.calendar.rsvp": { + rsvp: { + collection: "community.lexicon.calendar.rsvp", queryable: { status: {}, "subject.uri": {}, }, references: { event: { - collection: "community.lexicon.calendar.event", + collection: "event", // short name of the referenced collection field: "subject.uri", }, }, @@ -63,10 +65,10 @@ await contrail.init(); ```ts const { records, cursor } = await contrail.query( - "community.lexicon.calendar.event", + "event", // short name you declared in `collections` { filters: { mode: "in-person" }, - sort: { countType: "community.lexicon.calendar.rsvp", direction: "desc" }, + sort: { countType: "rsvp", direction: "desc" }, limit: 20, } ); @@ -219,11 +221,14 @@ Ingestion runs automatically via cron (`*/1 * * * *`). Schema is auto-initialize | `relations.*.groups` | — | Group value mappings (e.g. `{ going: "collection#going" }`) | | `relations.*.count` | `true` | Enable materialized count columns on the parent | | `references` | `{}` | Forward references to other collections for hydration | -| `references.*.collection` | — | Target collection NSID | +| `references.*.collection` | — | Short name of the target collection (key in `collections`) | | `references.*.field` | — | Field containing the target record's AT URI | | `queries` | `{}` | Custom query handlers (raw Response) | | `pipelineQueries` | `{}` | Custom query handlers that go through the standard filter/sort/hydration pipeline | | `searchable` | disabled | Full-text search fields. SQLite uses FTS5 virtual tables; PostgreSQL uses tsvector generated columns with GIN indexes. Provide `string[]` to enable, omit to disable | +| `collection` | — | Full NSID of the record type this short-name entry indexes (required) | +| `methods` | `["listRecords", "getRecord"]` | XRPC methods to emit for this collection | +| `allowInSpaces` | `true` | When spaces are enabled, emit a parallel `spaces_records_` table | ### Top-level options @@ -235,6 +240,8 @@ Ingestion runs automatically via cron (`*/1 * * * *`). Schema is auto-initialize | `relays` | Bluesky relays | Relay URLs for user discovery | | `jetstreams` | Bluesky Jetstream | Jetstream URLs for real-time ingestion | | `feeds` | — | Personalized feed configurations | +| `notify` | off | Expose `notifyOfUpdate`. `true` = open, string = `Authorization: Bearer ` required | +| `spaces` | — | Permissioned-spaces configuration. See [PERMISSIONED_DATA.md](./PERMISSIONED_DATA.md) | | `logger` | `console` | Logger instance (`{ log, warn, error }`) | ### Profiles @@ -243,16 +250,18 @@ Ingestion runs automatically via cron (`*/1 * * * *`). Schema is auto-initialize ## XRPC API -When using `createHandler`, all endpoints are available at `/xrpc/{nsid}.{method}`: +When using `createHandler`, all endpoints live under the deployment's own namespace at `/xrpc/{namespace}.{...}`: | Endpoint | Description | |----------|-------------| -| `{collection}.listRecords` | List/filter records | -| `{collection}.getRecord` | Get single record by URI | +| `{namespace}.{short}.listRecords` | List/filter records in a collection (keyed by its short name) | +| `{namespace}.{short}.getRecord` | Get single record by URI | | `{namespace}.getProfile` | Get a user's profile by DID or handle | | `{namespace}.notifyOfUpdate` | Notify of a record change for immediate indexing | | `{namespace}.getCursor` | Current cursor position | | `{namespace}.getOverview` | All collections summary | +| `{namespace}.permissionSet` | OAuth permission-set bundling every method above (auto-generated) | +| `{namespace}.space.*` | Spaces admin, invite, member, record XRPCs (when `spaces` is enabled) | ### Query parameters @@ -304,19 +313,19 @@ When using `createHandler`, all endpoints are available at `/xrpc/{nsid}.{method ``` # Upcoming events with 10+ going RSVPs, with RSVP records and profiles -/xrpc/community.lexicon.calendar.event.listRecords?startsAtMin=2026-03-16&rsvpsGoingCountMin=10&hydrateRsvps=5&profiles=true +/xrpc/com.example.event.listRecords?startsAtMin=2026-03-16&rsvpsGoingCountMin=10&hydrateRsvps=5&profiles=true # Events for a specific user (by handle) -/xrpc/community.lexicon.calendar.event.listRecords?actor=alice.bsky.social&profiles=true +/xrpc/com.example.event.listRecords?actor=alice.bsky.social&profiles=true # Single event with counts, RSVPs, and profiles -/xrpc/community.lexicon.calendar.event.getRecord?uri=at://did:plc:.../community.lexicon.calendar.event/...&hydrateRsvps=10&profiles=true +/xrpc/com.example.event.getRecord?uri=at://did:plc:.../community.lexicon.calendar.event/...&hydrateRsvps=10&profiles=true # Search for events by name/description -/xrpc/community.lexicon.calendar.event.listRecords?search=meetup&profiles=true +/xrpc/com.example.event.listRecords?search=meetup&profiles=true # RSVPs for a specific event, with the referenced event embedded -/xrpc/community.lexicon.calendar.rsvp.listRecords?subjectUri=at://did:plc:.../community.lexicon.calendar.event/...&hydrateEvent=true&profiles=true +/xrpc/com.example.rsvp.listRecords?subjectUri=at://did:plc:.../community.lexicon.calendar.event/...&hydrateEvent=true&profiles=true ``` ## Notify of Updates @@ -347,6 +356,36 @@ Contrail fetches the record from the user's PDS and figures out what to do: When Jetstream later delivers the same event, the duplicate is detected by CID and skipped. +## Permissioned Data + +Contrail ships an opt-in permissioned-spaces subsystem: an auth-gated store for records that can't live on public PDSes — private events, invite-only groups, members-only chat. Set `config.spaces` and contrail exposes the space XRPCs at `{namespace}.space.*` alongside your public indexer: + +```ts +const contrail = new Contrail({ + namespace: "com.example", + collections: { /* ... */ }, + spaces: { + type: "com.example.event.space", // NSID classifying the kind of space + serviceDid: "did:web:example.com", // your deployment's DID + // `resolver` is optional — defaults to a composite did:plc + did:web resolver. + }, +}); +``` + +Each collection you declare also gets a parallel `spaces_records_` table (opt out per-collection via `allowInSpaces: false`). Auth uses atproto service-auth JWTs via `@atcute/xrpc-server`. Access is a simple `read` / `write` permission per member — the space owner is implicit write. Invites are first-class (generated token, hashed-at-rest, expiry + max-uses + revocation). + +**Unified `listRecords`.** The per-collection `listRecords` endpoint accepts three call shapes: + +| Call | Returns | +| --- | --- | +| No auth, no `spaceUri` | Public records only | +| `?spaceUri=…` + service-auth JWT | Records from that one space (ACL-gated) | +| Service-auth JWT, no `spaceUri` | Public records **unioned** with records from every space the caller is a member of | + +The union path runs the public and per-space queries in parallel and merges with a shared keyset cursor, so filters, sorts (`time`, record-field, count), hydration, and references all work across sources. Records from a space carry a `space: ` field in the response. + +Full design, migration story, and known limits: [PERMISSIONED_DATA.md](./PERMISSIONED_DATA.md). + ## Typesafe Client Usage You can get fully typed XRPC queries for any Contrail instance using [`@atcute/lex-cli`](https://github.com/mary-ext/atcute). The lexicon files are committed to the repo, so you can pull them directly via the git source. @@ -395,7 +434,7 @@ import { Client } from "@atcute/client"; const rpc = new Client({ handler: simpleFetchHandler({ service: /* your contrail url */ }) }); -const response = await rpc.get("community.lexicon.calendar.event.getRecords", { +const response = await rpc.get("com.example.rsvp.listRecords", { params: { status: "going", limit: 10 }, // typed params }); diff --git a/app/config.ts b/app/config.ts index d5358f8..418d9a9 100644 --- a/app/config.ts +++ b/app/config.ts @@ -2,8 +2,13 @@ import type { ContrailConfig } from "../src/index"; export const config: ContrailConfig = { namespace: "rsvp.atmo", + spaces: { + type: "tools.atmo.event.space", + serviceDid: "did:web:rsvp.atmo", + }, collections: { - "community.lexicon.calendar.event": { + event: { + collection: "community.lexicon.calendar.event", queryable: { mode: {}, name: {}, @@ -15,7 +20,7 @@ export const config: ContrailConfig = { searchable: ["name", "description"], relations: { rsvps: { - collection: "community.lexicon.calendar.rsvp", + collection: "rsvp", groupBy: "status", count: true, countDistinct: "did", @@ -27,14 +32,15 @@ export const config: ContrailConfig = { }, }, }, - "community.lexicon.calendar.rsvp": { + rsvp: { + collection: "community.lexicon.calendar.rsvp", queryable: { status: {}, "subject.uri": {}, }, references: { event: { - collection: "community.lexicon.calendar.event", + collection: "event", field: "subject.uri", }, }, diff --git a/examples/cloudflare-workers/README.md b/examples/cloudflare-workers/README.md index 9a0e294..38bb787 100644 --- a/examples/cloudflare-workers/README.md +++ b/examples/cloudflare-workers/README.md @@ -38,19 +38,19 @@ Your XRPC API is now available at `http://localhost:8787`: ``` # List events sorted by RSVP count -/xrpc/community.lexicon.calendar.event.listRecords?sort=rsvpsCount +/xrpc/rsvp.atmo.event.listRecords?sort=rsvpsCount # Upcoming events with 10+ going RSVPs -/xrpc/community.lexicon.calendar.event.listRecords?startsAtMin=2026-03-16&rsvpsGoingCountMin=10 +/xrpc/rsvp.atmo.event.listRecords?startsAtMin=2026-03-16&rsvpsGoingCountMin=10 # Single event with hydrated RSVPs and profiles -/xrpc/community.lexicon.calendar.event.getRecord?uri=at://...&hydrateRsvps=10&profiles=true +/xrpc/rsvp.atmo.event.getRecord?uri=at://...&hydrateRsvps=10&profiles=true # Search events -/xrpc/community.lexicon.calendar.event.listRecords?search=meetup +/xrpc/rsvp.atmo.event.listRecords?search=meetup # RSVPs for a specific event -/xrpc/community.lexicon.calendar.rsvp.listRecords?subjectUri=at://... +/xrpc/rsvp.atmo.rsvp.listRecords?subjectUri=at://... ``` ## Deploy diff --git a/examples/cloudflare-workers/config.ts b/examples/cloudflare-workers/config.ts index 12630c0..a7169da 100644 --- a/examples/cloudflare-workers/config.ts +++ b/examples/cloudflare-workers/config.ts @@ -3,7 +3,8 @@ import type { ContrailConfig } from "@atmo-dev/contrail"; export const config: ContrailConfig = { namespace: "rsvp.atmo", collections: { - "community.lexicon.calendar.event": { + event: { + collection: "community.lexicon.calendar.event", queryable: { mode: {}, name: {}, @@ -15,7 +16,7 @@ export const config: ContrailConfig = { searchable: ["name", "description"], relations: { rsvps: { - collection: "community.lexicon.calendar.rsvp", + collection: "rsvp", groupBy: "status", count: true, groups: { @@ -26,14 +27,15 @@ export const config: ContrailConfig = { }, }, }, - "community.lexicon.calendar.rsvp": { + rsvp: { + collection: "community.lexicon.calendar.rsvp", queryable: { status: {}, "subject.uri": {}, }, references: { event: { - collection: "community.lexicon.calendar.event", + collection: "event", field: "subject.uri", }, }, diff --git a/examples/postgres/README.md b/examples/postgres/README.md index 12e1308..f4293f7 100644 --- a/examples/postgres/README.md +++ b/examples/postgres/README.md @@ -83,19 +83,19 @@ Your XRPC API is now available at `http://localhost:3000`: ``` # List events sorted by RSVP count -/xrpc/community.lexicon.calendar.event.listRecords?sort=rsvpsCount +/xrpc/rsvp.atmo.event.listRecords?sort=rsvpsCount # Upcoming events with 10+ going RSVPs -/xrpc/community.lexicon.calendar.event.listRecords?startsAtMin=2026-03-16&rsvpsGoingCountMin=10 +/xrpc/rsvp.atmo.event.listRecords?startsAtMin=2026-03-16&rsvpsGoingCountMin=10 # Single event with hydrated RSVPs and profiles -/xrpc/community.lexicon.calendar.event.getRecord?uri=at://...&hydrateRsvps=10&profiles=true +/xrpc/rsvp.atmo.event.getRecord?uri=at://...&hydrateRsvps=10&profiles=true # Search events -/xrpc/community.lexicon.calendar.event.listRecords?search=meetup +/xrpc/rsvp.atmo.event.listRecords?search=meetup # RSVPs for a specific event -/xrpc/community.lexicon.calendar.rsvp.listRecords?subjectUri=at://... +/xrpc/rsvp.atmo.rsvp.listRecords?subjectUri=at://... ``` ## Running everything together diff --git a/examples/postgres/config.ts b/examples/postgres/config.ts index 12630c0..a7169da 100644 --- a/examples/postgres/config.ts +++ b/examples/postgres/config.ts @@ -3,7 +3,8 @@ import type { ContrailConfig } from "@atmo-dev/contrail"; export const config: ContrailConfig = { namespace: "rsvp.atmo", collections: { - "community.lexicon.calendar.event": { + event: { + collection: "community.lexicon.calendar.event", queryable: { mode: {}, name: {}, @@ -15,7 +16,7 @@ export const config: ContrailConfig = { searchable: ["name", "description"], relations: { rsvps: { - collection: "community.lexicon.calendar.rsvp", + collection: "rsvp", groupBy: "status", count: true, groups: { @@ -26,14 +27,15 @@ export const config: ContrailConfig = { }, }, }, - "community.lexicon.calendar.rsvp": { + rsvp: { + collection: "community.lexicon.calendar.rsvp", queryable: { status: {}, "subject.uri": {}, }, references: { event: { - collection: "community.lexicon.calendar.event", + collection: "event", field: "subject.uri", }, }, diff --git a/examples/sveltekit-cloudflare-workers/README.md b/examples/sveltekit-cloudflare-workers/README.md index c32115b..4917318 100644 --- a/examples/sveltekit-cloudflare-workers/README.md +++ b/examples/sveltekit-cloudflare-workers/README.md @@ -21,7 +21,8 @@ import type { ContrailConfig } from '@atmo-dev/contrail'; export const config: ContrailConfig = { namespace: 'statusphere.app', collections: { - 'xyz.statusphere.status': { + status: { // short name → URL path segment + collection: 'xyz.statusphere.status', // full NSID of the record type queryable: { status: {}, // equality filter (?status=...) createdAt: { type: 'range' } // range filter (?createdAtMin=...&createdAtMax=...) @@ -59,7 +60,7 @@ npx wrangler deploy ```ts const client = getClient(platform!.env.DB); -const res = await client.get('xyz.statusphere.status.listRecords', { +const res = await client.get('statusphere.app.status.listRecords', { params: { limit: 50, profiles: true } // typed params }); res.data.records // typed response diff --git a/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getProfile.json b/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getProfile.json index a226862..4706150 100644 --- a/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getProfile.json +++ b/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/getProfile.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Get a user's profile by DID or handle", + "description": "Get a user's profiles by DID or handle", "parameters": { "type": "params", "required": [ @@ -21,8 +21,19 @@ "output": { "encoding": "application/json", "schema": { - "type": "ref", - "ref": "#profileEntry" + "type": "object", + "required": [ + "profiles" + ], + "properties": { + "profiles": { + "type": "array", + "items": { + "type": "ref", + "ref": "#profileEntry" + } + } + } } } }, diff --git a/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/permissionSet.json b/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/permissionSet.json new file mode 100644 index 0000000..232cfee --- /dev/null +++ b/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/permissionSet.json @@ -0,0 +1,26 @@ +{ + "lexicon": 1, + "id": "statusphere.app.permissionSet", + "defs": { + "main": { + "type": "permission-set", + "title": "statusphere.app", + "description": "All XRPC methods exposed by the statusphere.app service.", + "permissions": [ + { + "type": "permission", + "resource": "rpc", + "inheritAud": true, + "lxm": [ + "statusphere.app.getCursor", + "statusphere.app.getOverview", + "statusphere.app.getProfile", + "statusphere.app.notifyOfUpdate", + "statusphere.app.status.getRecord", + "statusphere.app.status.listRecords" + ] + } + ] + } + } +} diff --git a/examples/sveltekit-cloudflare-workers/lexicons-generated/xyz/statusphere/status/getRecord.json b/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/getRecord.json similarity index 98% rename from examples/sveltekit-cloudflare-workers/lexicons-generated/xyz/statusphere/status/getRecord.json rename to examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/getRecord.json index d13c602..da31528 100644 --- a/examples/sveltekit-cloudflare-workers/lexicons-generated/xyz/statusphere/status/getRecord.json +++ b/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/getRecord.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "xyz.statusphere.status.getRecord", + "id": "statusphere.app.status.getRecord", "defs": { "main": { "type": "query", diff --git a/examples/sveltekit-cloudflare-workers/lexicons-generated/xyz/statusphere/status/listRecords.json b/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/listRecords.json similarity index 99% rename from examples/sveltekit-cloudflare-workers/lexicons-generated/xyz/statusphere/status/listRecords.json rename to examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/listRecords.json index 69e24d7..8fb9309 100644 --- a/examples/sveltekit-cloudflare-workers/lexicons-generated/xyz/statusphere/status/listRecords.json +++ b/examples/sveltekit-cloudflare-workers/lexicons-generated/statusphere/app/status/listRecords.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "xyz.statusphere.status.listRecords", + "id": "statusphere.app.status.listRecords", "defs": { "main": { "type": "query", diff --git a/examples/sveltekit-cloudflare-workers/package.json b/examples/sveltekit-cloudflare-workers/package.json index e6760af..f1d962c 100644 --- a/examples/sveltekit-cloudflare-workers/package.json +++ b/examples/sveltekit-cloudflare-workers/package.json @@ -26,11 +26,11 @@ "@atcute/bluesky": "^3.3.0", "@atcute/client": "^4.2.1", "@atcute/identity-resolver": "^1.2.2", + "@atcute/lex-cli": "^2.5.3", + "@atcute/lexicon-doc": "^2.1.2", "@atcute/lexicons": "^1.2.9", "@atcute/oauth-node-client": "^1.1.0", "@atcute/tid": "^1.1.2", - "@atcute/lex-cli": "^2.5.3", - "@atcute/lexicon-doc": "^2.1.2", "@cloudflare/workers-types": "^4.20260317.1", "@eslint/compat": "^2.0.3", "@eslint/js": "^10.0.1", @@ -58,8 +58,8 @@ }, "license": "MIT", "dependencies": { - "@atmo-dev/contrail": "^0.0.3", "@atcute/jetstream": "^1.1.2", + "@atmo-dev/contrail": "^0.0.3", "@foxui/core": "^0.8.2", "@foxui/social": "^0.8.4", "@foxui/time": "^0.8.2", diff --git a/examples/sveltekit-cloudflare-workers/src/lib/contrail/config.ts b/examples/sveltekit-cloudflare-workers/src/lib/contrail/config.ts index 3311d34..b3aa09a 100644 --- a/examples/sveltekit-cloudflare-workers/src/lib/contrail/config.ts +++ b/examples/sveltekit-cloudflare-workers/src/lib/contrail/config.ts @@ -3,7 +3,8 @@ import type { ContrailConfig } from '@atmo-dev/contrail'; export const config: ContrailConfig = { namespace: 'statusphere.app', collections: { - 'xyz.statusphere.status': { + status: { + collection: 'xyz.statusphere.status', queryable: { status: {}, createdAt: { type: 'range' } diff --git a/examples/sveltekit-cloudflare-workers/src/routes/+page.server.ts b/examples/sveltekit-cloudflare-workers/src/routes/+page.server.ts index 059333a..a9bb158 100644 --- a/examples/sveltekit-cloudflare-workers/src/routes/+page.server.ts +++ b/examples/sveltekit-cloudflare-workers/src/routes/+page.server.ts @@ -6,7 +6,7 @@ export const load: PageServerLoad = async ({ platform }) => { try { const client = getServerClient(platform!.env.DB); - const res = await client.get('xyz.statusphere.status.listRecords', { + const res = await client.get('statusphere.app.status.listRecords', { params: { limit: 50, profiles: true, sort: 'createdAt', order: 'desc' } }); diff --git a/lexicons-generated/community/lexicon/calendar/event/getRecord.json b/lexicons-generated/rsvp/atmo/event/getRecord.json similarity index 92% rename from lexicons-generated/community/lexicon/calendar/event/getRecord.json rename to lexicons-generated/rsvp/atmo/event/getRecord.json index 894e157..dace81f 100644 --- a/lexicons-generated/community/lexicon/calendar/event/getRecord.json +++ b/lexicons-generated/rsvp/atmo/event/getRecord.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "community.lexicon.calendar.event.getRecord", + "id": "rsvp.atmo.event.getRecord", "defs": { "main": { "type": "query", @@ -20,6 +20,11 @@ "type": "boolean", "description": "Include profile + identity info keyed by DID" }, + "spaceUri": { + "type": "string", + "format": "at-uri", + "description": "If set, fetch from this permissioned space (requires service-auth JWT)." + }, "hydrateRsvps": { "type": "integer", "minimum": 1, @@ -65,6 +70,11 @@ "time_us": { "type": "integer" }, + "space": { + "type": "string", + "format": "at-uri", + "description": "Present when the record was read from a permissioned space; its value is the space URI." + }, "rsvpsCount": { "type": "integer", "description": "Total rsvps count" @@ -130,6 +140,11 @@ }, "time_us": { "type": "integer" + }, + "space": { + "type": "string", + "format": "at-uri", + "description": "Present when the record was read from a permissioned space." } } }, diff --git a/lexicons-generated/community/lexicon/calendar/event/listRecords.json b/lexicons-generated/rsvp/atmo/event/listRecords.json similarity index 93% rename from lexicons-generated/community/lexicon/calendar/event/listRecords.json rename to lexicons-generated/rsvp/atmo/event/listRecords.json index 873f649..582f31a 100644 --- a/lexicons-generated/community/lexicon/calendar/event/listRecords.json +++ b/lexicons-generated/rsvp/atmo/event/listRecords.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "community.lexicon.calendar.event.listRecords", + "id": "rsvp.atmo.event.listRecords", "defs": { "main": { "type": "query", @@ -26,6 +26,16 @@ "type": "boolean", "description": "Include profile + identity info keyed by DID" }, + "spaceUri": { + "type": "string", + "format": "at-uri", + "description": "If set, query records inside this permissioned space (requires service-auth JWT)." + }, + "byUser": { + "type": "string", + "format": "did", + "description": "Only used with spaceUri — filter to records authored by this DID." + }, "search": { "type": "string", "description": "Full-text search across: name, description" @@ -183,6 +193,11 @@ "time_us": { "type": "integer" }, + "space": { + "type": "string", + "format": "at-uri", + "description": "Present when the record was read from a permissioned space; its value is the space URI." + }, "rsvpsCount": { "type": "integer", "description": "Total rsvps count" @@ -239,6 +254,11 @@ }, "time_us": { "type": "integer" + }, + "space": { + "type": "string", + "format": "at-uri", + "description": "Present when the record was read from a permissioned space." } } }, diff --git a/lexicons-generated/rsvp/atmo/permissionSet.json b/lexicons-generated/rsvp/atmo/permissionSet.json new file mode 100644 index 0000000..835c1ac --- /dev/null +++ b/lexicons-generated/rsvp/atmo/permissionSet.json @@ -0,0 +1,42 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.permissionSet", + "defs": { + "main": { + "type": "permission-set", + "title": "rsvp.atmo", + "description": "All XRPC methods exposed by the rsvp.atmo service.", + "permissions": [ + { + "type": "permission", + "resource": "rpc", + "inheritAud": true, + "lxm": [ + "rsvp.atmo.event.getRecord", + "rsvp.atmo.event.listRecords", + "rsvp.atmo.getCursor", + "rsvp.atmo.getOverview", + "rsvp.atmo.getProfile", + "rsvp.atmo.notifyOfUpdate", + "rsvp.atmo.rsvp.getRecord", + "rsvp.atmo.rsvp.listRecords", + "rsvp.atmo.space.admin.addMember", + "rsvp.atmo.space.admin.createSpace", + "rsvp.atmo.space.admin.removeMember", + "rsvp.atmo.space.deleteRecord", + "rsvp.atmo.space.getRecord", + "rsvp.atmo.space.getSpace", + "rsvp.atmo.space.invite.create", + "rsvp.atmo.space.invite.list", + "rsvp.atmo.space.invite.redeem", + "rsvp.atmo.space.invite.revoke", + "rsvp.atmo.space.listMembers", + "rsvp.atmo.space.listRecords", + "rsvp.atmo.space.listSpaces", + "rsvp.atmo.space.putRecord" + ] + } + ] + } + } +} diff --git a/lexicons-generated/community/lexicon/calendar/rsvp/getRecord.json b/lexicons-generated/rsvp/atmo/rsvp/getRecord.json similarity index 91% rename from lexicons-generated/community/lexicon/calendar/rsvp/getRecord.json rename to lexicons-generated/rsvp/atmo/rsvp/getRecord.json index 9ab8d01..80ebe7c 100644 --- a/lexicons-generated/community/lexicon/calendar/rsvp/getRecord.json +++ b/lexicons-generated/rsvp/atmo/rsvp/getRecord.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "community.lexicon.calendar.rsvp.getRecord", + "id": "rsvp.atmo.rsvp.getRecord", "defs": { "main": { "type": "query", @@ -20,6 +20,11 @@ "type": "boolean", "description": "Include profile + identity info keyed by DID" }, + "spaceUri": { + "type": "string", + "format": "at-uri", + "description": "If set, fetch from this permissioned space (requires service-auth JWT)." + }, "hydrateEvent": { "type": "boolean", "description": "Embed the referenced event record" @@ -63,6 +68,11 @@ "time_us": { "type": "integer" }, + "space": { + "type": "string", + "format": "at-uri", + "description": "Present when the record was read from a permissioned space; its value is the space URI." + }, "event": { "type": "ref", "ref": "#refEventRecord" @@ -112,6 +122,11 @@ }, "time_us": { "type": "integer" + }, + "space": { + "type": "string", + "format": "at-uri", + "description": "Present when the record was read from a permissioned space." } } }, diff --git a/lexicons-generated/community/lexicon/calendar/rsvp/listRecords.json b/lexicons-generated/rsvp/atmo/rsvp/listRecords.json similarity index 90% rename from lexicons-generated/community/lexicon/calendar/rsvp/listRecords.json rename to lexicons-generated/rsvp/atmo/rsvp/listRecords.json index d0359bc..df28722 100644 --- a/lexicons-generated/community/lexicon/calendar/rsvp/listRecords.json +++ b/lexicons-generated/rsvp/atmo/rsvp/listRecords.json @@ -1,6 +1,6 @@ { "lexicon": 1, - "id": "community.lexicon.calendar.rsvp.listRecords", + "id": "rsvp.atmo.rsvp.listRecords", "defs": { "main": { "type": "query", @@ -26,6 +26,16 @@ "type": "boolean", "description": "Include profile + identity info keyed by DID" }, + "spaceUri": { + "type": "string", + "format": "at-uri", + "description": "If set, query records inside this permissioned space (requires service-auth JWT)." + }, + "byUser": { + "type": "string", + "format": "did", + "description": "Only used with spaceUri — filter to records authored by this DID." + }, "status": { "type": "string", "description": "Filter by status" @@ -120,6 +130,11 @@ "time_us": { "type": "integer" }, + "space": { + "type": "string", + "format": "at-uri", + "description": "Present when the record was read from a permissioned space; its value is the space URI." + }, "event": { "type": "ref", "ref": "#refEventRecord" @@ -160,6 +175,11 @@ }, "time_us": { "type": "integer" + }, + "space": { + "type": "string", + "format": "at-uri", + "description": "Present when the record was read from a permissioned space." } } }, diff --git a/lexicons-generated/rsvp/atmo/space/admin/addMember.json b/lexicons-generated/rsvp/atmo/space/admin/addMember.json new file mode 100644 index 0000000..2ea9e75 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/admin/addMember.json @@ -0,0 +1,60 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.admin.addMember", + "defs": { + "main": { + "type": "procedure", + "description": "Add a member to a space. Caller must be the space owner.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "did" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "did": { + "type": "string", + "format": "did" + }, + "perms": { + "type": "string", + "knownValues": [ + "read", + "write" + ], + "default": "write" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/space/admin/createSpace.json b/lexicons-generated/rsvp/atmo/space/admin/createSpace.json new file mode 100644 index 0000000..1ddedfc --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/admin/createSpace.json @@ -0,0 +1,59 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.admin.createSpace", + "defs": { + "main": { + "type": "procedure", + "description": "Create a new space owned by the JWT issuer. The caller is added as an owner-perm member.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "format": "nsid", + "description": "Space type NSID. Defaults to the service's configured type." + }, + "key": { + "type": "string", + "description": "Space key. Auto-generated (TID) if omitted." + }, + "memberListRef": { + "type": "string", + "format": "at-uri" + }, + "appPolicyRef": { + "type": "string", + "format": "at-uri" + }, + "appPolicy": { + "type": "ref", + "ref": "rsvp.atmo.space.defs#appPolicy" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "space" + ], + "properties": { + "space": { + "type": "ref", + "ref": "rsvp.atmo.space.defs#spaceView" + } + } + } + }, + "errors": [ + { + "name": "AlreadyExists" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/space/admin/removeMember.json b/lexicons-generated/rsvp/atmo/space/admin/removeMember.json new file mode 100644 index 0000000..11d4893 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/admin/removeMember.json @@ -0,0 +1,55 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.admin.removeMember", + "defs": { + "main": { + "type": "procedure", + "description": "Remove a member from a space. Owner only. Cannot remove the owner.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "did" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "did": { + "type": "string", + "format": "did" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + }, + { + "name": "InvalidRequest" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/space/defs.json b/lexicons-generated/rsvp/atmo/space/defs.json new file mode 100644 index 0000000..788a843 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/defs.json @@ -0,0 +1,193 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.defs", + "description": "Shared types for permissioned-space XRPC methods.", + "defs": { + "spaceView": { + "type": "object", + "required": [ + "uri", + "ownerDid", + "type", + "key", + "serviceDid", + "createdAt" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "ownerDid": { + "type": "string", + "format": "did" + }, + "type": { + "type": "string", + "format": "nsid" + }, + "key": { + "type": "string" + }, + "serviceDid": { + "type": "string" + }, + "memberListRef": { + "type": "string", + "format": "at-uri" + }, + "appPolicyRef": { + "type": "string", + "format": "at-uri" + }, + "createdAt": { + "type": "integer" + }, + "appPolicy": { + "type": "ref", + "ref": "#appPolicy", + "description": "Owner-only" + } + } + }, + "memberView": { + "type": "object", + "required": [ + "did", + "perms", + "addedAt" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "perms": { + "type": "string", + "knownValues": [ + "read", + "write" + ], + "description": "'write' implies 'read'. Space owner is always implicit write." + }, + "addedAt": { + "type": "integer" + }, + "addedBy": { + "type": "string", + "format": "did" + } + } + }, + "recordView": { + "type": "object", + "required": [ + "spaceUri", + "collection", + "authorDid", + "rkey", + "record", + "createdAt" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "authorDid": { + "type": "string", + "format": "did" + }, + "rkey": { + "type": "string" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "record": { + "type": "unknown" + }, + "createdAt": { + "type": "integer" + } + } + }, + "appPolicy": { + "type": "object", + "required": [ + "mode", + "apps" + ], + "properties": { + "mode": { + "type": "string", + "knownValues": [ + "allow", + "deny" + ], + "description": "'allow' = default-allow with apps[] as denylist; 'deny' = default-deny with apps[] as allowlist." + }, + "apps": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "inviteView": { + "type": "object", + "required": [ + "tokenHash", + "spaceUri", + "perms", + "usedCount", + "createdBy", + "createdAt" + ], + "properties": { + "tokenHash": { + "type": "string" + }, + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "perms": { + "type": "string", + "knownValues": [ + "read", + "write" + ] + }, + "expiresAt": { + "type": "integer" + }, + "maxUses": { + "type": "integer" + }, + "usedCount": { + "type": "integer" + }, + "createdBy": { + "type": "string", + "format": "did" + }, + "createdAt": { + "type": "integer" + }, + "revokedAt": { + "type": "integer" + }, + "note": { + "type": "string" + } + } + } + } +} diff --git a/lexicons-generated/rsvp/atmo/space/deleteRecord.json b/lexicons-generated/rsvp/atmo/space/deleteRecord.json new file mode 100644 index 0000000..c78bed7 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/deleteRecord.json @@ -0,0 +1,56 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.deleteRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Delete a record from a space. Callers can delete their own records; the space owner can delete any record (via a separate admin path).", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "collection", + "rkey" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/space/getRecord.json b/lexicons-generated/rsvp/atmo/space/getRecord.json new file mode 100644 index 0000000..2b96fe4 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/getRecord.json @@ -0,0 +1,59 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.getRecord", + "defs": { + "main": { + "type": "query", + "description": "Get a single record from a space.", + "parameters": { + "type": "params", + "required": [ + "spaceUri", + "collection", + "author", + "rkey" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "author": { + "type": "string", + "format": "did" + }, + "rkey": { + "type": "string" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "record" + ], + "properties": { + "record": { + "type": "ref", + "ref": "rsvp.atmo.space.defs#recordView" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/space/getSpace.json b/lexicons-generated/rsvp/atmo/space/getSpace.json new file mode 100644 index 0000000..6bf9973 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/getSpace.json @@ -0,0 +1,45 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.getSpace", + "defs": { + "main": { + "type": "query", + "description": "Get metadata for a single space. Caller must be a member or the owner.", + "parameters": { + "type": "params", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "space" + ], + "properties": { + "space": { + "type": "ref", + "ref": "rsvp.atmo.space.defs#spaceView" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/space/invite/create.json b/lexicons-generated/rsvp/atmo/space/invite/create.json new file mode 100644 index 0000000..0131640 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/invite/create.json @@ -0,0 +1,74 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.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.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "perms": { + "type": "string", + "knownValues": [ + "read", + "write" + ], + "default": "write" + }, + "expiresAt": { + "type": "integer", + "description": "Unix ms timestamp. Omit for no expiry." + }, + "maxUses": { + "type": "integer", + "minimum": 1, + "description": "Omit for unlimited uses." + }, + "note": { + "type": "string", + "maxLength": 500 + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "token", + "invite" + ], + "properties": { + "token": { + "type": "string", + "description": "Raw token. Shown once — cannot be retrieved later." + }, + "invite": { + "type": "ref", + "ref": "rsvp.atmo.space.defs#inviteView" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/space/invite/list.json b/lexicons-generated/rsvp/atmo/space/invite/list.json new file mode 100644 index 0000000..848d4e2 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/invite/list.json @@ -0,0 +1,52 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.invite.list", + "defs": { + "main": { + "type": "query", + "description": "List invites for a space. Owner only.", + "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.space.defs#inviteView" + } + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/space/invite/redeem.json b/lexicons-generated/rsvp/atmo/space/invite/redeem.json new file mode 100644 index 0000000..9a41ce0 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/invite/redeem.json @@ -0,0 +1,52 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.invite.redeem", + "defs": { + "main": { + "type": "procedure", + "description": "Redeem an invite token. The JWT issuer becomes a member of the space with the invite's perms.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "perms" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "perms": { + "type": "string", + "knownValues": [ + "read", + "write" + ] + } + } + } + }, + "errors": [ + { + "name": "InvalidInvite" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/space/invite/revoke.json b/lexicons-generated/rsvp/atmo/space/invite/revoke.json new file mode 100644 index 0000000..afa34f2 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/invite/revoke.json @@ -0,0 +1,51 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.invite.revoke", + "defs": { + "main": { + "type": "procedure", + "description": "Revoke an invite. Owner only. Invites are identified by their tokenHash (visible in list output).", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "tokenHash" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "tokenHash": { + "type": "string" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/space/listMembers.json b/lexicons-generated/rsvp/atmo/space/listMembers.json new file mode 100644 index 0000000..b1b88b4 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/listMembers.json @@ -0,0 +1,48 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.listMembers", + "defs": { + "main": { + "type": "query", + "description": "List members of a space. Caller must be a member or the owner.", + "parameters": { + "type": "params", + "required": [ + "spaceUri" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "members" + ], + "properties": { + "members": { + "type": "array", + "items": { + "type": "ref", + "ref": "rsvp.atmo.space.defs#memberView" + } + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/space/listRecords.json b/lexicons-generated/rsvp/atmo/space/listRecords.json new file mode 100644 index 0000000..3121b4a --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/listRecords.json @@ -0,0 +1,70 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.listRecords", + "defs": { + "main": { + "type": "query", + "description": "List records of a given collection within a space. Access is governed by the space's collection policy.", + "parameters": { + "type": "params", + "required": [ + "spaceUri", + "collection" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "byUser": { + "type": "string", + "format": "did", + "description": "Only return records authored by this DID." + }, + "cursor": { + "type": "string" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "records" + ], + "properties": { + "records": { + "type": "array", + "items": { + "type": "ref", + "ref": "rsvp.atmo.space.defs#recordView" + } + }, + "cursor": { + "type": "string" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons-generated/rsvp/atmo/space/listSpaces.json b/lexicons-generated/rsvp/atmo/space/listSpaces.json new file mode 100644 index 0000000..39f2992 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/listSpaces.json @@ -0,0 +1,57 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.listSpaces", + "defs": { + "main": { + "type": "query", + "description": "List spaces the caller has access to. Default scope is 'member' (spaces the caller is a member of, including owned); 'owner' lists only spaces the caller owns.", + "parameters": { + "type": "params", + "properties": { + "scope": { + "type": "string", + "knownValues": [ + "member", + "owner" + ], + "default": "member" + }, + "type": { + "type": "string", + "format": "nsid" + }, + "cursor": { + "type": "string" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaces" + ], + "properties": { + "spaces": { + "type": "array", + "items": { + "type": "ref", + "ref": "rsvp.atmo.space.defs#spaceView" + } + }, + "cursor": { + "type": "string" + } + } + } + } + } + } +} diff --git a/lexicons-generated/rsvp/atmo/space/putRecord.json b/lexicons-generated/rsvp/atmo/space/putRecord.json new file mode 100644 index 0000000..65f9863 --- /dev/null +++ b/lexicons-generated/rsvp/atmo/space/putRecord.json @@ -0,0 +1,68 @@ +{ + "lexicon": 1, + "id": "rsvp.atmo.space.putRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Write a record into a space. The author is always the JWT issuer. If rkey is omitted, a TID is generated.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "spaceUri", + "collection", + "record" + ], + "properties": { + "spaceUri": { + "type": "string", + "format": "at-uri" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "record": { + "type": "unknown" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "rkey", + "authorDid", + "createdAt" + ], + "properties": { + "rkey": { + "type": "string" + }, + "authorDid": { + "type": "string", + "format": "did" + }, + "createdAt": { + "type": "integer" + } + } + } + }, + "errors": [ + { + "name": "NotFound" + }, + { + "name": "Forbidden" + } + ] + } + } +} diff --git a/lexicons/README.md b/lexicons/README.md new file mode 100644 index 0000000..45e1224 --- /dev/null +++ b/lexicons/README.md @@ -0,0 +1,16 @@ +# 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. +- **`spaces-lexicon-templates/`** — Shipped library templates for permissioned spaces; + the generator instantiates them under `.space.*` into `lexicons-generated/`. + +All three (`lexicons/`, `lexicons-pulled/`, `lexicons-generated/`) are picked up by +`lex-cli generate` (see `lex.config.js` `files` glob). diff --git a/package.json b/package.json index 9e3de5c..875971c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "sideEffects": false, "files": [ "dist", - "lexicons/tools/atmo/**/*.json" + "spaces-lexicon-templates/**/*.json" ], "exports": { ".": { @@ -29,11 +29,11 @@ "types": "./dist/generate.d.ts", "import": "./dist/generate.js" }, - "./lexicons": { - "types": "./dist/lexicons.d.ts", - "import": "./dist/lexicons.js" + "./publish": { + "types": "./dist/publish.d.ts", + "import": "./dist/publish.js" }, - "./lexicons/*.json": "./lexicons/*.json" + "./spaces-lexicon-templates/*.json": "./spaces-lexicon-templates/*.json" }, "repository": { "type": "git", @@ -56,6 +56,7 @@ "clean": "tsx scripts/clean.ts", "generate": "tsx app/generate.ts", "generate:pull": "tsx app/generate.ts && lex-cli pull && tsx app/generate.ts && lex-cli pull && lex-cli generate", + "publish-lexicons": "tsx scripts/publish-lexicons.ts", "typecheck": "tsc --noEmit", "ingest": "curl -s http://localhost:8787/__scheduled?cron=*/1+*+*+*+*", "sync": "tsx app/sync.ts", @@ -68,7 +69,7 @@ "@atcute/identity": "^1.1.4", "@atcute/identity-resolver": "^1.2.2", "@atcute/jetstream": "^1.0.2", - "@atcute/lexicons": "^1.2.7", + "@atcute/lexicons": "^1.2.9", "@atcute/xrpc-server": "^0.1.12", "hono": "^4.12.8" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33b19f0..af3fd0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^1.0.2 version: 1.1.2 '@atcute/lexicons': - specifier: ^1.2.7 - version: 1.2.7 + specifier: ^1.2.9 + version: 1.2.9 '@atcute/xrpc-server': specifier: ^0.1.12 version: 0.1.12 @@ -70,11 +70,141 @@ importers: specifier: ^4.63.0 version: 4.63.0(@cloudflare/workers-types@4.20260205.0) + examples/sveltekit-cloudflare-workers: + dependencies: + '@atcute/jetstream': + specifier: ^1.1.2 + version: 1.1.2 + '@atmo-dev/contrail': + specifier: link:../.. + version: link:../.. + '@foxui/core': + specifier: ^0.8.2 + version: 0.8.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) + '@foxui/social': + specifier: ^0.8.4 + version: 0.8.6(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) + '@foxui/time': + specifier: ^0.8.2 + version: 0.8.4(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) + valibot: + specifier: ^1.3.1 + version: 1.3.1(typescript@6.0.2) + devDependencies: + '@atcute/atproto': + specifier: ^3.1.10 + version: 3.1.10 + '@atcute/bluesky': + specifier: ^3.3.0 + version: 3.3.2 + '@atcute/client': + specifier: ^4.2.1 + version: 4.2.1 + '@atcute/identity-resolver': + specifier: ^1.2.2 + version: 1.2.2(@atcute/identity@1.1.4) + '@atcute/lex-cli': + specifier: ^2.5.3 + version: 2.5.3 + '@atcute/lexicon-doc': + specifier: ^2.1.2 + version: 2.1.2 + '@atcute/lexicons': + specifier: ^1.2.9 + version: 1.2.9 + '@atcute/oauth-node-client': + specifier: ^1.1.0 + version: 1.1.0 + '@atcute/tid': + specifier: ^1.1.2 + version: 1.1.2 + '@cloudflare/workers-types': + specifier: ^4.20260317.1 + version: 4.20260414.1 + '@eslint/compat': + specifier: ^2.0.3 + version: 2.0.5(eslint@10.2.0(jiti@2.6.1)) + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.2.0(jiti@2.6.1)) + '@sveltejs/adapter-cloudflare': + specifier: ^7.2.8 + version: 7.2.8(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.82.2(@cloudflare/workers-types@4.20260414.1)) + '@sveltejs/kit': + specifier: ^2.55.0 + version: 2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + '@sveltejs/vite-plugin-svelte': + specifier: ^7.0.0 + version: 7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + '@tailwindcss/forms': + specifier: ^0.5.11 + version: 0.5.11(tailwindcss@4.2.2) + '@tailwindcss/vite': + specifier: ^4.2.2 + version: 4.2.2(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + bits-ui: + specifier: ^2.16.4 + version: 2.17.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + eslint: + specifier: ^10.1.0 + version: 10.2.0(jiti@2.6.1) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-svelte: + specifier: ^3.16.0 + version: 3.17.0(eslint@10.2.0(jiti@2.6.1))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + globals: + specifier: ^17.4.0 + version: 17.5.0 + prettier: + specifier: ^3.8.1 + version: 3.8.1 + prettier-plugin-svelte: + specifier: ^3.5.1 + version: 3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + prettier-plugin-tailwindcss: + specifier: ^0.7.2 + version: 0.7.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2)))(prettier@3.8.1) + svelte: + specifier: ^5.55.0 + version: 5.55.4(@typescript-eslint/types@8.58.2) + svelte-check: + specifier: ^4.4.5 + version: 4.4.6(picomatch@4.0.4)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2) + tailwindcss: + specifier: ^4.2.2 + version: 4.2.2 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^6.0.2 + version: 6.0.2 + typescript-eslint: + specifier: ^8.57.2 + version: 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + vite: + specifier: ^8.0.3 + version: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + wrangler: + specifier: ^4.78.0 + version: 4.82.2(@cloudflare/workers-types@4.20260414.1) + packages: '@atcute/atproto@3.1.10': resolution: {integrity: sha512-+GKZpOc0PJcdWMQEkTfg/rSNDAAHxmAUGBl60g2az15etqJn5WaUPNGFE2sB7hKpwi5Ue2h/L0OacINcE/JDDQ==} + '@atcute/atproto@3.1.11': + resolution: {integrity: sha512-yh+ASvA+iHHQij6UeHEKp2+rwvFvQR8A6/5Dk/xvqDslIikWEFx9VlprNwm/clQIPl2bLuQg+LHS8uY9o5nFTA==} + + '@atcute/bluesky-richtext-segmenter@3.0.0': + resolution: {integrity: sha512-NhZTUKtFpeBBbILwAcxj5u4RobIoHOmGw3CAaaEFNebKYSvmTecrXJ7XufHw5DFOUdr8SiKXQVRQxGAxulMNWg==} + + '@atcute/bluesky@3.3.2': + resolution: {integrity: sha512-BqVXghoNFbBm38hveWqPQQ25VZxHXmqYk5h0NG8YFiwNLrI8Ww1nXHAJzS65zyHwV5QeSA6EDFaTrQwm+7cCFQ==} + '@atcute/car@5.1.1': resolution: {integrity: sha512-MeRUJNXYgAHrJZw7mMoZJb9xIqv3LZLQw90rRRAVAo8SGNdICwyqe6Bf2LGesX73QM04MBuYO6Kqhvold3TFfg==} @@ -87,9 +217,6 @@ packages: '@atcute/client@4.2.1': resolution: {integrity: sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw==} - '@atcute/crypto@2.4.0': - resolution: {integrity: sha512-XtEeDaSgfr92C7b1VDRvd3F9pI8tVUyy8PJAeu8IWQC7+e/GXZOSl58uWh5YP/9p1Lsa0I16uKHwogygxEwlMQ==} - '@atcute/crypto@2.4.1': resolution: {integrity: sha512-tJ3Pi/XYcAsABKtqSlSOTKfO5YiQ4XdqlTuPS8HiRZSezOPcXBFFzAFWpSIJPURbVPFQL3LLrrK0Ea24wl5qeQ==} @@ -117,26 +244,38 @@ packages: '@atcute/identity': ^1.1.0 '@atcute/identity-resolver': ^1.1.3 - '@atcute/lexicons@1.2.7': - resolution: {integrity: sha512-gCvkSMI1F1zx7xXa59iPiSKMH3L5Hga6iurGqQjaQbE2V/np/2QuDqQzt96TNbWfaFAXE9f9oY+0z3ljf/bweA==} - '@atcute/lexicons@1.2.9': resolution: {integrity: sha512-/RRHm2Cw9o8Mcsrq0eo8fjS9okKYLGfuFwrQ0YoP/6sdSDsXshaTLJsvLlcUcaDaSJ1YFOuHIo3zr2Om2F/16g==} + '@atcute/lexicons@1.3.0': + resolution: {integrity: sha512-Eq5y+9onnCXNVUlNiMf31beSXHKqptB7lUo/68YbhlmxdaR7ooywHmahya9goP5AsmlYEA1z+dRPXIDAa9O7cg==} + '@atcute/mst@1.0.0': resolution: {integrity: sha512-pMce2efib+dmKtnGnIvJZitVncJkpr3AmhyfgfYllni8KzsaDGsJmuGavSVpuojAhQe+6jYwHFtpm/beiiH4uw==} - '@atcute/multibase@1.1.8': - resolution: {integrity: sha512-pJgtImMZKCjqwRbu+2GzB+4xQjKBXDwdZOzeqe0u97zYKRGftpGYGvYv3+pMe2xXe+msDyu7Nv8iJp+U14otTA==} - '@atcute/multibase@1.2.0': resolution: {integrity: sha512-ZK2GRra+qIYq9nNuQB52m2ul0hOmCQEtPobGfTSUxm7pF0OGEkWGkWHugFhNEDVzHzTwPxHp6VGotdZFue4lYQ==} + '@atcute/oauth-crypto@0.1.0': + resolution: {integrity: sha512-qZYDCNLF/4B6AndYT1rsQelN8621AC5u/sL5PHvlr/qqAbmmUwCBGjEgRSyZtHE1AqD60VNiSMlOgAuEQTSl3w==} + + '@atcute/oauth-keyset@0.1.0': + resolution: {integrity: sha512-+wqT/+I5Lg9VzKnKY3g88+N45xbq+wsdT6bHDGqCVa2u57gRvolFF4dY+weMfc/OX641BIZO6/o+zFtKBsMQnQ==} + + '@atcute/oauth-node-client@1.1.0': + resolution: {integrity: sha512-xCp/VfjtvTeKscKR/oI2hdMTp1/DaF/7ll8b6yZOCgbKlVDDfhCn5mmKNVARGTNaoywxrXG3XffbWCIx3/E87w==} + + '@atcute/oauth-types@0.1.1': + resolution: {integrity: sha512-u+3KMjse3Uc/9hDyilu1QVN7IpcnjVXgRzhddzBB8Uh6wePHNVBDdi9wQvFTVVA3zmxtMJVptXRyLLg6Ou9bqg==} + '@atcute/repo@0.1.3': resolution: {integrity: sha512-kN4gkrkQgJwI5xkVQ9zSdI8ULR47uH48EdJdjMvKAWsSDu8zKYd8ZQ6n778qa6o3WYEq02v9QVf7pzeU+3fYVA==} - '@atcute/uint8array@1.1.0': - resolution: {integrity: sha512-JtHXIVW6LPU9FMWp7SgE4HbUs3uV2WdfkK/2RWdEGjr4EgMV50P3FdU6fPeGlTfDNBJVYMIsuD2wwaKRPV/Aqg==} + '@atcute/tid@1.1.2': + resolution: {integrity: sha512-bmPuOX/TOfcm/vsK9vM98spjkcx2wgd9S2PeK5oLgEr8IbNRPq7iMCAPzOL1nu5XAW3LlkOYQEbYRcw5vcQ37w==} + + '@atcute/time-ms@1.3.2': + resolution: {integrity: sha512-F+qOyR9pO55g1d/QmN+Gr+fimoUQQLusdGSB6pjV0wW5KPILR4oQ4e2ZhWzqUbeHLAgWvgoTTMsMDdz62Xa2tg==} '@atcute/uint8array@1.1.1': resolution: {integrity: sha512-3LsC8XB8TKe9q/5hOA5sFuzGaIFdJZJNewC5OKa3o/eU6+K7JR6see9Zy2JbQERNVnRl11EzbNov1efgLMAs4g==} @@ -144,12 +283,12 @@ packages: '@atcute/util-fetch@1.0.5': resolution: {integrity: sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig==} - '@atcute/util-text@1.1.0': - resolution: {integrity: sha512-34G9KD5Z9f7oEdFpZOmqrMnU86p8ne6LlxJowfZzKNszRcl1GH+FtEPh3N1woelJT2SkPXMK2anwT8DESTluwA==} - '@atcute/util-text@1.1.1': resolution: {integrity: sha512-JH0SxzUQJAmbOBTYyhxQbkkI6M33YpjlVLEcbP5GYt43xgFArzV0FJVmEpvIj0kjsmphHB45b6IitdvxPdec9w==} + '@atcute/util-text@1.2.0': + resolution: {integrity: sha512-b8WSh+Z7K601eUFFmTFj8QPKDO8Ic0VDDj63sdKzpkm+ySQKsYT5nXekViGqFVKbyKj1V5FyvZvgXad6/aI4QQ==} + '@atcute/varint@2.0.0': resolution: {integrity: sha512-CEY/oVK/nVpL4e5y3sdenLETDL6/Xu5xsE/0TupK+f0Yv8jcD60t2gD8SHROWSvUwYLdkjczLCSA7YrtnjCzWw==} @@ -232,39 +371,81 @@ packages: workerd: optional: true + '@cloudflare/unenv-preset@2.16.0': + resolution: {integrity: sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0 + peerDependenciesMeta: + workerd: + optional: true + '@cloudflare/workerd-darwin-64@1.20260205.0': resolution: {integrity: sha512-ToOItqcirmWPwR+PtT+Q4bdjTn/63ZxhJKEfW4FNn7FxMTS1Tw5dml0T0mieOZbCpcvY8BdvPKFCSlJuI8IVHQ==} engines: {node: '>=16'} cpu: [x64] os: [darwin] + '@cloudflare/workerd-darwin-64@1.20260410.1': + resolution: {integrity: sha512-0sh6xPmCKUfv/lUklP1dfyeKxCuEZGS0HeduxnucL8ECxSgAdWTOD42h/lQTwZCIiWtyHB+ZNB9hsS2Mlf0tMQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20260205.0': resolution: {integrity: sha512-402ZqLz+LrG0NDXp7Hn7IZbI0DyhjNfjAlVenb0K3yod9KCuux0u3NksNBvqJx0mIGHvVR4K05h+jfT5BTHqGA==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] + '@cloudflare/workerd-darwin-arm64@1.20260410.1': + resolution: {integrity: sha512-r2On29gPvlk/eiH/OpeUT23xoB8W8D1PHr8lul5nyxElLqvh3yNxZUnJWrbcOl+ubfrvw7+jFwgopMe17xyf0g==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + '@cloudflare/workerd-linux-64@1.20260205.0': resolution: {integrity: sha512-rz9jBzazIA18RHY+osa19hvsPfr0LZI1AJzIjC6UqkKKphcTpHBEQ25Xt8cIA34ivMIqeENpYnnmpDFesLkfcQ==} engines: {node: '>=16'} cpu: [x64] os: [linux] + '@cloudflare/workerd-linux-64@1.20260410.1': + resolution: {integrity: sha512-qWORRcAzPZeHJjrcYBNZTN6Y9l+iZQUz4KBdWbNrM6My4CpNrXS5kErPR373vG//5QPaDGwMXgBqyn9xfzarJQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + '@cloudflare/workerd-linux-arm64@1.20260205.0': resolution: {integrity: sha512-jr6cKpMM/DBEbL+ATJ9rYue758CKp0SfA/nXt5vR32iINVJrb396ye9iat2y9Moa/PgPKnTrFgmT6urUmG3IUg==} engines: {node: '>=16'} cpu: [arm64] os: [linux] + '@cloudflare/workerd-linux-arm64@1.20260410.1': + resolution: {integrity: sha512-jQfuHL4mnGDFyomSS3JNs9TpTvCu6Vzz2QSNCfJRstMzTICUFLMc4Vp/xKK+M5xkb0PoAu/G0hHx7jrxB2j+OQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + '@cloudflare/workerd-windows-64@1.20260205.0': resolution: {integrity: sha512-SMPW5jCZYOG7XFIglSlsgN8ivcl0pCrSAYxCwxtWvZ88whhcDB/aISNtiQiDZujPH8tIo2hE5dEkxW7tGEwc3A==} engines: {node: '>=16'} cpu: [x64] os: [win32] + '@cloudflare/workerd-windows-64@1.20260410.1': + resolution: {integrity: sha512-h8q/nbheDqpknY7AAOz19MuQkZAR1/bnoZnKipyeUPXt5No+y6HlTtva9Bohx5Fhc1MW2CX2MQVdb55qtkkqZQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + '@cloudflare/workers-types@4.20260205.0': resolution: {integrity: sha512-LTnpvcodmiuMwxmbrO2Fd0+Avbm2UVLLJxT8J2pRWPfoM44gmbIecXwOPZmDAMeadKWrBsQ+B0sloQAhUu5fpA==} + '@cloudflare/workers-types@4.20260414.1': + resolution: {integrity: sha512-E2wgYT1ywoM1M68nmVpxKdKzXsZm5vOu2plsqUixlK7YIydqsw31dZ+EjwXnAsdEjLaYC6XfsJayil8AEhyaBQ==} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -272,168 +453,442 @@ packages: '@emnapi/core@1.9.0': resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + '@emnapi/runtime@1.8.1': resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + '@emnapi/wasi-threads@1.2.0': resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.27.0': resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.27.0': resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.27.0': resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.27.0': resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.27.0': resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.27.0': resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.27.0': resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.0': resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.27.0': resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.27.0': resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.27.0': resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.27.0': resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.27.0': resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.27.0': resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.27.0': resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.27.0': resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.27.0': resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.27.0': resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.0': resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.27.0': resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.0': resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.27.0': resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.27.0': resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.27.0': resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.27.0': resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.27.0': resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/compat@2.0.5': + resolution: {integrity: sha512-IbHDbHJfkVNv6xjlET8AIVo/K1NQt7YT4Rp6ok/clyBGcpRx1l6gv0Rq3vBvYfPJIZt6ODf66Zq08FJNDpnzgg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^8.40 || 9 || 10 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.5.5': + resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.1': + resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@fontsource-variable/geist-mono@5.2.7': + resolution: {integrity: sha512-ZKlZ5sjtalb2TwXKs400mAGDlt/+2ENLNySPx0wTz3bP3mWARCsUW+rpxzZc7e05d2qGch70pItt3K4qttbIYA==} + + '@fontsource-variable/geist@5.2.8': + resolution: {integrity: sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==} + + '@foxui/core@0.8.3': + resolution: {integrity: sha512-dQE+fhwDT4SlUhFFGs8+609S9rBC+gyY3Tjd48jIQtJai3hOAIVxFFB+j/ngNiTzMME3oDV7yc6H/OAghTy8Eg==} + peerDependencies: + svelte: '>=5' + tailwindcss: '>=3' + + '@foxui/core@0.9.0': + resolution: {integrity: sha512-+zzya5pclKhyQYKdAAdpIGa6GG/oipvBi6PM/T/zynXmHEdSm6bWqHtlcL98Ulvp4cmML7VM9OtkvXVk4NeP/w==} + peerDependencies: + svelte: '>=5' + tailwindcss: '>=3' + + '@foxui/social@0.8.6': + resolution: {integrity: sha512-Hk78zsc8/1sHzQTBiy95Kvu05k7YQTLZeqRjk/L0bZsjMItNRik8ocheKHEK4f01L8ZaOmxnREVhf4P2nUm/1w==} + peerDependencies: + svelte: '>=5' + tailwindcss: '>=3' + + '@foxui/text@0.8.4': + resolution: {integrity: sha512-eW5EJE9BSgJa1kx9u95960OTIKcVsO6+nFidpdvdBak1fMoIlZTb/HL2nlPG21HbWewBIys7th0SC5B4wMWfxw==} + peerDependencies: + svelte: '>=5' + tailwindcss: '>=3' + + '@foxui/time@0.8.4': + resolution: {integrity: sha512-HOa2DqHtOMSd5JDv9XZ+AVFnmJ5Oi1wnK+dCfbd/+2LXhuQKqg5kQcclyX9Ufhprwuhx59JO7JUe4Vix8jBmpw==} + peerDependencies: + svelte: '>=5' + tailwindcss: '>=3' + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + '@img/colour@1.0.0': resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} @@ -580,9 +1035,20 @@ packages: '@types/node': optional: true + '@internationalized/date@3.12.0': + resolution: {integrity: sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==} + + '@jis3r/icons@2.7.0': + resolution: {integrity: sha512-RXee+W1AoWFHtFliLZ7SQxVopAezmCH0xU45HKTvvpDQ+6Y3g6tUHvKOK16J52TM8YsN4WDnsiOmcwau+bHmwg==} + peerDependencies: + svelte: ^5.0.0 + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -596,6 +1062,11 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@lucide/svelte@1.8.0': + resolution: {integrity: sha512-+zYQUKqEOVP5lxbGmxL1OVgGMQtRK91eIJ0bR+3Cr1ts4oQEsQfxyzzd5X47psJlblAuGFrl2xm4YuATjR9oaA==} + peerDependencies: + svelte: ^5 + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -611,6 +1082,12 @@ packages: '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@napi-rs/wasm-runtime@1.1.3': + resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@noble/secp256k1@3.0.0': resolution: {integrity: sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==} @@ -626,6 +1103,11 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@number-flow/svelte@0.4.0': + resolution: {integrity: sha512-9tnowrlZlBV3IVe3Gm1V7yXSf4Ugag2k7iW45xqb04HXSa1ApEImopvGWAjJpHDvS849o+UCb0YH461Mtde9lA==} + peerDependencies: + svelte: ^4 || ^5 + '@optique/core@0.6.11': resolution: {integrity: sha512-GVLFihzBA1j78NFlkU5N1Lu0jRqET0k6Z66WK8VQKG/a3cxmCInVGSKMIdQG8i6pgC8wD5OizF6Y3QMztmhAxg==} engines: {bun: '>=1.2.0', deno: '>=2.3.0', node: '>=20.0.0'} @@ -641,6 +1123,12 @@ packages: '@oxc-project/types@0.115.0': resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} + '@oxc-project/types@0.124.0': + resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@poppinss/colors@4.1.6': resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} @@ -650,95 +1138,190 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@remirror/core-constants@3.0.0': + resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + + '@rolldown/binding-android-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@rolldown/binding-android-arm64@1.0.0-rc.9': resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@rolldown/binding-darwin-arm64@1.0.0-rc.9': resolution: {integrity: sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@rolldown/binding-darwin-x64@1.0.0-rc.9': resolution: {integrity: sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@rolldown/binding-freebsd-x64@1.0.0-rc.9': resolution: {integrity: sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': resolution: {integrity: sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': resolution: {integrity: sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': resolution: {integrity: sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': resolution: {integrity: sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': resolution: {integrity: sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': resolution: {integrity: sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': resolution: {integrity: sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': resolution: {integrity: sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==} engines: {node: '>=14.0.0'} cpu: [wasm32] + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': resolution: {integrity: sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': resolution: {integrity: sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@rolldown/pluginutils@1.0.0-rc.15': + resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + '@rolldown/pluginutils@1.0.0-rc.9': resolution: {integrity: sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==} @@ -877,18 +1460,351 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@sveltejs/acorn-typescript@1.0.9': + resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/adapter-cloudflare@7.2.8': + resolution: {integrity: sha512-bIdhY/Fi4AQmqiBdQVKnafH1h9Gw+xbCvHyUu4EouC8rJOU02zwhi14k/FDhQ0mJF1iblIu3m8UNQ8GpGIvIOQ==} + peerDependencies: + '@sveltejs/kit': ^2.0.0 + wrangler: ^4.0.0 + + '@sveltejs/kit@2.57.1': + resolution: {integrity: sha512-VRdSbB96cI1EnRh09CqmnQqP/YJvET5buj8S6k7CxaJqBJD4bw4fRKDjcarAj/eX9k2eHifQfDH8NtOh+ZxxPw==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.3.3 || ^6.0.0 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + typescript: + optional: true + + '@sveltejs/vite-plugin-svelte@7.0.0': + resolution: {integrity: sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.46.4 + vite: ^8.0.0-beta.7 || ^8.0.0 + + '@swc/helpers@0.5.21': + resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} + + '@tailwindcss/forms@0.5.11': + resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' + + '@tailwindcss/node@4.2.2': + resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} + + '@tailwindcss/oxide-android-arm64@4.2.2': + resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.2': + resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.2': + resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.2': + resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tiptap/core@3.22.3': + resolution: {integrity: sha512-Dv9MKK5BDWCF0N2l6/Pxv3JNCce2kwuWf2cKMBc2bEetx0Pn6o7zlFmSxMvYK4UtG1Tw9Yg/ZHi6QOFWK0Zm9Q==} + peerDependencies: + '@tiptap/pm': ^3.22.3 + + '@tiptap/extension-blockquote@3.22.3': + resolution: {integrity: sha512-IaUx3zh7yLHXzIXKL+fw/jzFhsIImdhJyw0lMhe8FfYrefFqXJFYW/sey6+L/e8B3AWvTksPA6VBwefzbH77JA==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-bold@3.22.3': + resolution: {integrity: sha512-tysipHla2zCWr8XNIWRaW9O+7i7/SoEqnRqSRUUi2ailcJjlia+RBy3RykhkgyThrQDStu5KGBS/UvrXwA+O1A==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-bubble-menu@3.22.3': + resolution: {integrity: sha512-Y6zQjh0ypDg32HWgICEvmPSKjGLr39k3aDxxt/H0uQEZSfw4smT0hxUyyyjVjx68C6t6MTnwdfz0hPI5lL68vQ==} + peerDependencies: + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + + '@tiptap/extension-bullet-list@3.22.3': + resolution: {integrity: sha512-xOmW/b1hgECIE6r3IeZvKn4VVlG3+dfTjCWE6lnnyLaqdNkNhKS1CwUmDZdYNLUS2ryIUtgz5ID1W/8A3PhbiA==} + peerDependencies: + '@tiptap/extension-list': ^3.22.3 + + '@tiptap/extension-code-block-lowlight@3.22.3': + resolution: {integrity: sha512-NGFuD9zb1QfnCgD2zW4XaUEdQvd/ydm9FmXXh8eawx/+C8xt3p21DIKRERvxWrCvvVNzIUEpZRRYehPOJiD1eg==} + peerDependencies: + '@tiptap/core': ^3.22.3 + '@tiptap/extension-code-block': ^3.22.3 + '@tiptap/pm': ^3.22.3 + highlight.js: ^11 + lowlight: ^2 || ^3 + + '@tiptap/extension-code-block@3.22.3': + resolution: {integrity: sha512-RiQtEjDAPrHpdo6sw6b7fOw/PijqgFIsozKKkGcSeBgWHQuFg7q9OxJTj+l0e60rVwSu/5gmKEEobzM9bX+t2Q==} + peerDependencies: + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + + '@tiptap/extension-code@3.22.3': + resolution: {integrity: sha512-wafWTDQOuMKtXpZEuk1PFQmzopabBciNLryL90MB9S03MNLaQQZYLnmYkDBlzAaLAbgF5QiC+2XZQEBQuTVjFQ==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-document@3.22.3': + resolution: {integrity: sha512-MCSr1PFPtTd++lA3H1RNgqAczAE59XXJ5wUFIQf2F+/0DPY5q2SU4g5QsNJVxPPft5mrNT4C6ty8xBPrALFEdA==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-dropcursor@3.22.3': + resolution: {integrity: sha512-taXq9Tl5aybdFbptJtFRHX9LFJzbXphAbPp4/vutFyTrBu5meXDxuS+B9pEmE+Or0XcolTlW2nDZB0Tqnr18JQ==} + peerDependencies: + '@tiptap/extensions': ^3.22.3 + + '@tiptap/extension-floating-menu@3.22.3': + resolution: {integrity: sha512-0f8b4KZ3XKai8GXWseIYJGdOfQr3evtFbBo3U08zy2aYzMMXWG0zEF7qe5/oiYp2aZ95edjjITnEceviTsZkIg==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + + '@tiptap/extension-gapcursor@3.22.3': + resolution: {integrity: sha512-L/Px4UeQEVG/D9WIlcAOIej+4wyIBCMUSYicSR+hW68UsObe4rxVbUas1QgidQKm6DOhoT7U7D4KQHA/Gdg/7A==} + peerDependencies: + '@tiptap/extensions': ^3.22.3 + + '@tiptap/extension-hard-break@3.22.3': + resolution: {integrity: sha512-J0v8I99y9tbvVmgKYKzKP/JYNsWaZYS7avn4rzLft2OhnyTfwt3OoY8DtpHmmi6apSUaCtoWHWta/TmoEfK1nQ==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-heading@3.22.3': + resolution: {integrity: sha512-XBHuhiEV2EEhZHpOLcplLqAmBIhJciU3I6AtwmqeEqDC0P114uMEfAO7JGlbBZdCYotNer26PKnu44TBTeNtkw==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-history@3.22.3': + resolution: {integrity: sha512-3kev2iP84YPjwUk8BaM28EQiVHbBmE0lkcIjh/6yvgRxRwblzRwyJuFcsVOY5ouBVxl9Rg1rcFFCAs8CFNFkUg==} + peerDependencies: + '@tiptap/extensions': ^3.22.3 + + '@tiptap/extension-horizontal-rule@3.22.3': + resolution: {integrity: sha512-wI2bFzScs+KgWeBH/BtypcVKeYelCyqV0RG8nxsZMWtPrBhqixzNd0Oi3gEKtjSjKUqMQ/kjJAIRuESr5UzlHA==} + peerDependencies: + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + + '@tiptap/extension-image@3.22.3': + resolution: {integrity: sha512-Qpp8c5LOQaNpHrzjqZtoxtIR+8sSqJ7k8v+8anmYw3nxjvt2kpfT28Vd7aWMX55ZS43LaxMx+MkZqbmgUmMP0w==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-italic@3.22.3': + resolution: {integrity: sha512-LteA4cb4EGCiUtrK2JHvDF/Zg0/YqV4DUyHhAAho+oGEQDupZlsS6m0ia5wQcclkiTLzsoPrwcSNu6RDGQ16wQ==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-link@3.22.3': + resolution: {integrity: sha512-S8/P2o9pv6B3kqLjH2TRWwSAximGbciNc6R8/QcN6HWLYxp0N0JoqN3rZHl9VWIBAGRWc4zkt80dhqrl2xmgfQ==} + peerDependencies: + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + + '@tiptap/extension-list-item@3.22.3': + resolution: {integrity: sha512-80CNf4oO5y8+LdckT4CyMe1t01EyhpRrQC9H45JW20P7559Nrchp5my3vvMtIAJbpTPPZtcB7LwdzWGKsG5drg==} + peerDependencies: + '@tiptap/extension-list': ^3.22.3 + + '@tiptap/extension-list-keymap@3.22.3': + resolution: {integrity: sha512-pKuyj5llu35zd/s2u/H9aydKZjmPRAIK5P1q/YXULhhCNln2RnmuRfQ5NklAqTD3yGciQ2lxDwwf7J6iw3ergA==} + peerDependencies: + '@tiptap/extension-list': ^3.22.3 + + '@tiptap/extension-list@3.22.3': + resolution: {integrity: sha512-rqvv/dtqwbX+8KnPv0eMYp6PnBcuhPMol5cv1GlS8Nq/Cxt68EWGUHBuTFesw+hdnRQLmKwzoO1DlRn7PhxYRQ==} + peerDependencies: + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + + '@tiptap/extension-ordered-list@3.22.3': + resolution: {integrity: sha512-orAghtmd+K4Euu4BgI1hG+iZDXBYOyl5YTwiLBc2mQn+pqtZ9LqaH2us4ETwEwNP3/IWXGSAimUZ19nuL+eM2w==} + peerDependencies: + '@tiptap/extension-list': ^3.22.3 + + '@tiptap/extension-paragraph@3.22.3': + resolution: {integrity: sha512-oO7rhfyhEuwm+50s9K3GZPjYyEEEvFAvm1wXopvZnhbkBLydIWImBfrZoC5IQh4/sRDlTIjosV2C+ji5y0tUSg==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-placeholder@3.22.3': + resolution: {integrity: sha512-7vbtlDVO00odqCnsMSmA4b6wjL5PFdfExFsdsDO0K0VemqHZ/doIRx/tosNUD1VYSOyKQd8U7efUjkFyVoIPlg==} + peerDependencies: + '@tiptap/extensions': ^3.22.3 + + '@tiptap/extension-strike@3.22.3': + resolution: {integrity: sha512-jY2InoUlKkuk5KHoIDGdML1OCA2n6PRHAtxwHNkAmiYh0Khf0zaVPGFpx4dgQrN7W5Q1WE6oBZnjrvy6qb7w0g==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-text@3.22.3': + resolution: {integrity: sha512-Q9R7JsTdomP5uUjtPjNKxHT1xoh/i9OJZnmgJLe7FcgZEaPOQ3bWxmKZoLZQfDfZjyB8BtH+Hc7nUvhCMOePxw==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-typography@3.22.3': + resolution: {integrity: sha512-QQv1KUHsr3LTGhtPIG/ZP15Cj5VmI5ChgK4Q7UZ7JZBppBKAeMnyAnOcO2ith+qZ8qp5s+UiQ7Unp0zP1VSYkg==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extension-underline@3.22.3': + resolution: {integrity: sha512-Ch6CBWRa5w90yYSPUW6x9Py9JdrXMqk3pZ9OIlMYD8A7BqyZGfiHerX7XDMYDS09KjyK3U9XH60/zxYOzXdDLA==} + peerDependencies: + '@tiptap/core': ^3.22.3 + + '@tiptap/extensions@3.22.3': + resolution: {integrity: sha512-s5eiMq0m5N6N+W7dU6rd60KgZyyCD7FvtPNNswISfPr12EQwJBfbjWwTqd0UKNzA4fNrhQEERXnzORkykttPeA==} + peerDependencies: + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + + '@tiptap/markdown@3.22.3': + resolution: {integrity: sha512-Ajw8AkAae7IET1sxZqNv+dhZQSuLY0AHWocowTny1azyiBtmRRKygkGK0ysVA+k6UxmBD6K+tvnbpjQ0/ACl4A==} + peerDependencies: + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + + '@tiptap/pm@3.22.3': + resolution: {integrity: sha512-NjfWjZuvrqmpICT+GZWNIjtOdhPyqFKDMtQy7tsQ5rErM9L2ZQdy/+T/BKSO1JdTeBhdg9OP+0yfsqoYp2aT6A==} + + '@tiptap/starter-kit@3.22.3': + resolution: {integrity: sha512-vdW/Oo1fdwTL1VOQ5YYbTov00ANeHLquBVEZyL/EkV7Xv5io9rXQsCysJfTSHhiQlyr2MtWFB4+CPGuwXjQWOQ==} + + '@tiptap/suggestion@3.22.3': + resolution: {integrity: sha512-m2c+5gDj2vW7UI1J4JHCKehQUVE12qBhgF+DC+WEWUU8ZrFNf5OEYWQHDNsopa5RRpilfKfhPNbMtXgvGOsk6g==} + peerDependencies: + '@tiptap/core': ^3.22.3 + '@tiptap/pm': ^3.22.3 + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -898,6 +1814,77 @@ packages: '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@typescript-eslint/eslint-plugin@8.58.2': + resolution: {integrity: sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.58.2 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.58.2': + resolution: {integrity: sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.58.2': + resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.58.2': + resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.58.2': + resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.58.2': + resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.58.2': + resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.58.2': + resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.58.2': + resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.58.2': + resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/vanilla@10.3.1': + resolution: {integrity: sha512-lT4scGLu59ovA3zmtUonukAGcA0AdOOh+iwNDS05Bsu7Lq9aZToDHhI6D8Q2qvsVraovtsLLYwPrWdG/noMAKw==} + '@vitest/expect@4.1.0': resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} @@ -927,11 +1914,19 @@ packages: '@vitest/utils@4.1.0': resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} hasBin: true + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -949,6 +1944,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -957,13 +1956,32 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + bits-ui@2.17.3: + resolution: {integrity: sha512-Bef41uY9U2jaBJHPhcPvmBNkGec5Wx2z6eioDsTmsaR2vH4QoaOcPi75gzCG3+/2TNr6v/qBwzgWNPYCxNtrEA==} + engines: {node: '>=20'} + peerDependencies: + '@internationalized/date': ^3.8.1 + svelte: ^5.33.0 + blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -989,6 +2007,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1003,14 +2025,32 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + custom-event-polyfill@1.0.7: + resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1020,6 +2060,17 @@ packages: supports-color: optional: true + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -1028,14 +2079,34 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devalue@5.7.1: + resolution: {integrity: sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + dompurify@3.4.0: + resolution: {integrity: sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==} + + emoji-picker-element@1.29.1: + resolution: {integrity: sha512-TOiHzu9Dqib3x4MwcAi3wi3RdyT4SoeB4b15AvH1ks4SBwTl7DeebhZ0d3x6dNi4XfNU7IGRZ7NBQllj0RqwrQ==} + + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} @@ -1047,17 +2118,104 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-svelte@3.17.0: + resolution: {integrity: sha512-sF6wgd5FLS2P8CCaOy2HdYYYEcZ6TwL251dLHUkNmtLnWECk1Dwc+j6VeulmmnFxr7Xs0WNtjweOA+bJ0PnaFw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.1 || ^9.0.0 || ^10.0.0 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.2.0: + resolution: {integrity: sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + esm-env@1.2.2: resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrap@2.2.5: + resolution: {integrity: sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + event-target-polyfill@0.0.4: resolution: {integrity: sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==} @@ -1068,10 +2226,19 @@ packages: extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1084,6 +2251,10 @@ packages: picomatch: optional: true + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1092,11 +2263,22 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} fs-extra@8.1.0: @@ -1115,6 +2297,18 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + globals@17.5.0: + resolution: {integrity: sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==} + engines: {node: '>=18'} + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -1122,6 +2316,13 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + + hls.js@1.6.16: + resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==} + hono@4.12.8: resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==} engines: {node: '>=16.9.0'} @@ -1138,6 +2339,20 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + is-emoji-supported@0.0.5: + resolution: {integrity: sha512-WOlXUhDDHxYqcSmFZis+xWhhqXiK2SU0iYiqmth5Ip0FHLZQAt9rKL5ahnilE8/86WH8tZ3bmNNNC+bTzamqlw==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1150,6 +2365,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} @@ -1161,6 +2379,10 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -1173,13 +2395,32 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -1250,6 +2491,10 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -1257,20 +2502,55 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + loadjs@4.3.0: + resolution: {integrity: sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + lowlight@3.3.0: + resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + + marked@17.0.6: + resolution: {integrity: sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==} + engines: {node: '>= 20'} + hasBin: true + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1279,18 +2559,40 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mini-svg-data-uri@1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + miniflare@4.20260205.0: resolution: {integrity: sha512-jG1TknEDeFqcq/z5gsOm1rKeg4cNG7ruWxEuiPxl3pnQumavxo8kFpeQC6XKVpAhh2PI9ODGyIYlgd77sTHl5g==} engines: {node: '>=18.0.0'} hasBin: true + miniflare@4.20260410.0: + resolution: {integrity: sha512-94LEU8d+XPVGp18eW4+bu1v7Tnq7srhqWMIsrx2jhSkdbTnGqg1I613R0GKY4eygBYl9MbqXEhzK/bczJb6uMg==} + engines: {node: '>=18.0.0'} + hasBin: true + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + mode-watcher@1.1.0: + resolution: {integrity: sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==} + peerDependencies: + svelte: ^5.27.0 + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1307,6 +2609,12 @@ packages: engines: {node: ^18 || >=20} hasBin: true + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + number-flow@0.6.0: + resolution: {integrity: sha512-K8flNq2Wqus53vjp/btVo3qXFkagF8dIdYavreBfE7hlvFFG/b1HMGEH6nZL+mlrJ+4lbLP9OmPv3t2rmRkpSQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1314,6 +2622,13 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} @@ -1325,10 +2640,18 @@ packages: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + p-map@2.1.0: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} @@ -1406,6 +2729,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} @@ -1417,6 +2744,21 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + plyr@3.8.4: + resolution: {integrity: sha512-DrzLbK9Wol3zeiuZCleD9aUOl0KAaBHR9H6WVVVYPZ4Ya+LYxUFTgSF1jooHcMQCv96Ws96wCaZzIoP3bES8pQ==} + + postcss-load-config@3.1.4: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -1435,6 +2777,22 @@ packages: yaml: optional: true + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + postcss@8.5.8: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} @@ -1455,6 +2813,71 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-plugin-svelte@3.5.1: + resolution: {integrity: sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==} + peerDependencies: + prettier: ^3.0.0 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + + prettier-plugin-tailwindcss@0.7.2: + resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} + engines: {node: '>=20.19'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-hermes': '*' + '@prettier/plugin-oxc': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-multiline-arrays: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-hermes': + optional: true + '@prettier/plugin-oxc': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-svelte: + optional: true + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -1465,12 +2888,81 @@ packages: engines: {node: '>=14'} hasBin: true + prosemirror-changeset@2.4.1: + resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==} + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} + + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-markdown@1.13.4: + resolution: {integrity: sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==} + + prosemirror-menu@1.3.0: + resolution: {integrity: sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==} + + prosemirror-model@1.25.4: + resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} + + prosemirror-schema-basic@1.2.4: + resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + + prosemirror-transform@1.12.0: + resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} + + prosemirror-view@1.41.8: + resolution: {integrity: sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + rangetouch@2.0.1: + resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==} + read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} @@ -1479,6 +2971,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + regexparam@3.0.0: + resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} + engines: {node: '>=8'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -1490,6 +2986,11 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rolldown@1.0.0-rc.15: + resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rolldown@1.0.0-rc.9: resolution: {integrity: sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1500,9 +3001,40 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + runed@0.23.4: + resolution: {integrity: sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==} + peerDependencies: + svelte: ^5.7.0 + + runed@0.25.0: + resolution: {integrity: sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==} + peerDependencies: + svelte: ^5.7.0 + + runed@0.28.0: + resolution: {integrity: sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==} + peerDependencies: + svelte: ^5.7.0 + + runed@0.35.1: + resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==} + peerDependencies: + '@sveltejs/kit': ^2.21.0 + svelte: ^5.7.0 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -1511,6 +3043,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1530,6 +3065,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -1566,6 +3105,9 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -1575,6 +3117,77 @@ packages: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} + svelte-check@4.4.6: + resolution: {integrity: sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte-eslint-parser@1.6.0: + resolution: {integrity: sha512-qoB1ehychT6OxEtQAqc/guSqLS20SlA53Uijl7x375s8nlUT0lb9ol/gzraEEatQwsyPTJo87s2CmKL9Xab+Uw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.30.3} + peerDependencies: + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + svelte-sonner@1.1.0: + resolution: {integrity: sha512-3lYM6ZIqWe+p9vwwWHGWP/ZdvHiUtzURsud2quIxivrX4rvpXh6i+geBGn0m3JS6KwW6W8VgbOl3xQMcDuh6gg==} + peerDependencies: + svelte: ^5.0.0 + + svelte-tiptap@3.0.1: + resolution: {integrity: sha512-Vi3kVGOd01f7mslOxGbJB7z2QavdvH+6WffhB+Y5fleTiZaW0YWqIboyO2u/uh4BQeosiINmmuRJ+Qwb7mYP+A==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.0.0 + '@tiptap/extension-bubble-menu': ^3.0.0 + '@tiptap/extension-floating-menu': ^3.0.0 + '@tiptap/pm': ^3.0.0 + svelte: ^5.0.0 + + svelte-toolbelt@0.10.6: + resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.30.2 + + svelte-toolbelt@0.7.1: + resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.0.0 + + svelte@5.55.4: + resolution: {integrity: sha512-q8DFohk6vUswSng95IZb9nzWJnbINZsK7OiM1snAa3qCjJBL0ZQpvMyAaVXjUukdM75J/m8UE8xwqat8Ors/zQ==} + engines: {node: '>=18'} + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwind-variants@3.2.2: + resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==} + engines: {node: '>=16.x', pnpm: '>=7.x'} + peerDependencies: + tailwind-merge: '>=3.0.0' + tailwindcss: '*' + peerDependenciesMeta: + tailwind-merge: + optional: true + + tailwindcss@4.2.2: + resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} + + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + engines: {node: '>=6'} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -1608,10 +3221,20 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -1642,15 +3265,34 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + typescript-eslint@8.58.2: + resolution: {integrity: sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} @@ -1661,6 +3303,10 @@ packages: resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} engines: {node: '>=20.18.1'} + undici@7.24.4: + resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} + engines: {node: '>=20.18.1'} + unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -1671,6 +3317,23 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-polyfill@1.1.14: + resolution: {integrity: sha512-p4f3TTAG6ADVF3mwbXw7hGw+QJyw5CnNGvYh5fCuQQZIiuKUswqcznyV3pGDP9j0TSmC4UvRKm8kl1QsX1diiQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + valibot@1.3.1: + resolution: {integrity: sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vite@8.0.0: resolution: {integrity: sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1714,6 +3377,57 @@ packages: yaml: optional: true + vite@8.0.8: + resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + vitest@4.1.0: resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -1749,6 +3463,26 @@ packages: jsdom: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + web-haptics@0.0.6: + resolution: {integrity: sha512-eCzcf1LDi20+Fr0x9V3OkX92k0gxEQXaHajmhXHitsnk6SxPeshv8TBtBRqxyst8HI1uf2FyFVE7QS3jo1gkrw==} + peerDependencies: + react: '>=18' + react-dom: '>=18' + svelte: '>=4' + vue: '>=3' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + svelte: + optional: true + vue: + optional: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1759,11 +3493,24 @@ packages: engines: {node: '>=8'} hasBin: true + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + workerd@1.20260205.0: resolution: {integrity: sha512-CcMH5clHwrH8VlY7yWS9C/G/C8g9czIz1yU3akMSP9Z3CkEMFSoC3GGdj5G7Alw/PHEeez1+1IrlYger4pwu+w==} engines: {node: '>=16'} hasBin: true + workerd@1.20260410.1: + resolution: {integrity: sha512-T/GRD6Y5vN9g4CnGmOlfST1w7bj+1IjRFvX0K7CodZPJuPVPNPGhz8Wppah0WdT6A7I8Kad3zgZ2OkDdWtENrg==} + engines: {node: '>=16'} + hasBin: true + + worktop@0.8.0-next.18: + resolution: {integrity: sha512-+TvsA6VAVoMC3XDKR5MoC/qlLqDixEfOBysDEKnPIPou/NvoPWCAuXHXMsswwlvmEuvX56lQjvELLyLuzTKvRw==} + engines: {node: '>=12'} + wrangler@4.63.0: resolution: {integrity: sha512-+R04jF7Eb8K3KRMSgoXpcIdLb8GC62eoSGusYh1pyrSMm/10E0hbKkd7phMJO4HxXc6R7mOHC5SSoX9eof30Uw==} engines: {node: '>=20.0.0'} @@ -1774,11 +3521,21 @@ packages: '@cloudflare/workers-types': optional: true - ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} - engines: {node: '>=10.0.0'} + wrangler@4.82.2: + resolution: {integrity: sha512-SKfW21sTJUkM/Qd8zc9oc8TBkAWHRsXuTxE6XdToC55Ct84pR+IfRdaTjCTuC0dL+KYvauSvSn2rtqS2Ae+Dcw==} + engines: {node: '>=20.3.0'} + hasBin: true peerDependencies: - bufferutil: ^4.0.1 + '@cloudflare/workers-types': ^4.20260410.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 utf-8-validate: '>=5.0.2' peerDependenciesMeta: bufferutil: @@ -1790,6 +3547,14 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + yocto-queue@1.2.2: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} @@ -1800,11 +3565,25 @@ packages: youch@4.1.0-beta.10: resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + snapshots: '@atcute/atproto@3.1.10': dependencies: - '@atcute/lexicons': 1.2.7 + '@atcute/lexicons': 1.2.9 + + '@atcute/atproto@3.1.11': + dependencies: + '@atcute/lexicons': 1.3.0 + + '@atcute/bluesky-richtext-segmenter@3.0.0': {} + + '@atcute/bluesky@3.3.2': + dependencies: + '@atcute/atproto': 3.1.11 + '@atcute/lexicons': 1.3.0 '@atcute/car@5.1.1': dependencies: @@ -1816,24 +3595,18 @@ snapshots: '@atcute/cbor@2.3.2': dependencies: '@atcute/cid': 2.4.1 - '@atcute/multibase': 1.1.8 + '@atcute/multibase': 1.2.0 '@atcute/uint8array': 1.1.1 '@atcute/cid@2.4.1': dependencies: - '@atcute/multibase': 1.1.8 + '@atcute/multibase': 1.2.0 '@atcute/uint8array': 1.1.1 '@atcute/client@4.2.1': dependencies: '@atcute/identity': 1.1.4 - '@atcute/lexicons': 1.2.7 - - '@atcute/crypto@2.4.0': - dependencies: - '@atcute/multibase': 1.1.8 - '@atcute/uint8array': 1.1.1 - '@noble/secp256k1': 3.0.0 + '@atcute/lexicons': 1.2.9 '@atcute/crypto@2.4.1': dependencies: @@ -1855,7 +3628,7 @@ snapshots: '@atcute/jetstream@1.1.2': dependencies: - '@atcute/lexicons': 1.2.7 + '@atcute/lexicons': 1.2.9 '@badrap/valita': 0.4.6 '@mary-ext/event-iterator': 1.0.0 '@mary-ext/simple-event-emitter': 1.0.0 @@ -1886,7 +3659,7 @@ snapshots: '@atcute/lexicon-resolver@0.1.6(@atcute/identity-resolver@1.2.2(@atcute/identity@1.1.4))(@atcute/identity@1.1.4)': dependencies: - '@atcute/crypto': 2.4.0 + '@atcute/crypto': 2.4.1 '@atcute/identity': 1.1.4 '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) '@atcute/lexicon-doc': 2.1.2 @@ -1895,17 +3668,17 @@ snapshots: '@atcute/util-fetch': 1.0.5 '@badrap/valita': 0.4.6 - '@atcute/lexicons@1.2.7': + '@atcute/lexicons@1.2.9': dependencies: - '@atcute/uint8array': 1.1.0 - '@atcute/util-text': 1.1.0 + '@atcute/uint8array': 1.1.1 + '@atcute/util-text': 1.1.1 '@standard-schema/spec': 1.1.0 esm-env: 1.2.2 - '@atcute/lexicons@1.2.9': + '@atcute/lexicons@1.3.0': dependencies: '@atcute/uint8array': 1.1.1 - '@atcute/util-text': 1.1.1 + '@atcute/util-text': 1.2.0 '@standard-schema/spec': 1.1.0 esm-env: 1.2.2 @@ -1915,25 +3688,56 @@ snapshots: '@atcute/cid': 2.4.1 '@atcute/uint8array': 1.1.1 - '@atcute/multibase@1.1.8': + '@atcute/multibase@1.2.0': dependencies: '@atcute/uint8array': 1.1.1 - '@atcute/multibase@1.2.0': + '@atcute/oauth-crypto@0.1.0': dependencies: + '@atcute/multibase': 1.2.0 '@atcute/uint8array': 1.1.1 + '@badrap/valita': 0.4.6 + nanoid: 5.1.7 + + '@atcute/oauth-keyset@0.1.0': + dependencies: + '@atcute/oauth-crypto': 0.1.0 + + '@atcute/oauth-node-client@1.1.0': + dependencies: + '@atcute/client': 4.2.1 + '@atcute/identity': 1.1.4 + '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) + '@atcute/lexicons': 1.2.9 + '@atcute/oauth-crypto': 0.1.0 + '@atcute/oauth-keyset': 0.1.0 + '@atcute/oauth-types': 0.1.1 + '@atcute/util-fetch': 1.0.5 + '@badrap/valita': 0.4.6 + nanoid: 5.1.7 + + '@atcute/oauth-types@0.1.1': + dependencies: + '@atcute/identity': 1.1.4 + '@atcute/lexicons': 1.2.9 + '@atcute/oauth-keyset': 0.1.0 + '@badrap/valita': 0.4.6 '@atcute/repo@0.1.3': dependencies: '@atcute/car': 5.1.1 '@atcute/cbor': 2.3.2 '@atcute/cid': 2.4.1 - '@atcute/crypto': 2.4.0 + '@atcute/crypto': 2.4.1 '@atcute/lexicons': 1.2.9 '@atcute/mst': 1.0.0 '@atcute/uint8array': 1.1.1 - '@atcute/uint8array@1.1.0': {} + '@atcute/tid@1.1.2': + dependencies: + '@atcute/time-ms': 1.3.2 + + '@atcute/time-ms@1.3.2': {} '@atcute/uint8array@1.1.1': {} @@ -1941,11 +3745,11 @@ snapshots: dependencies: '@badrap/valita': 0.4.6 - '@atcute/util-text@1.1.0': + '@atcute/util-text@1.1.1': dependencies: unicode-segmenter: 0.14.5 - '@atcute/util-text@1.1.1': + '@atcute/util-text@1.2.0': dependencies: unicode-segmenter: 0.14.5 @@ -2118,23 +3922,46 @@ snapshots: optionalDependencies: workerd: 1.20260205.0 + '@cloudflare/unenv-preset@2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260410.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260410.1 + '@cloudflare/workerd-darwin-64@1.20260205.0': optional: true + '@cloudflare/workerd-darwin-64@1.20260410.1': + optional: true + '@cloudflare/workerd-darwin-arm64@1.20260205.0': optional: true + '@cloudflare/workerd-darwin-arm64@1.20260410.1': + optional: true + '@cloudflare/workerd-linux-64@1.20260205.0': optional: true + '@cloudflare/workerd-linux-64@1.20260410.1': + optional: true + '@cloudflare/workerd-linux-arm64@1.20260205.0': optional: true + '@cloudflare/workerd-linux-arm64@1.20260410.1': + optional: true + '@cloudflare/workerd-windows-64@1.20260205.0': optional: true + '@cloudflare/workerd-windows-64@1.20260410.1': + optional: true + '@cloudflare/workers-types@4.20260205.0': {} + '@cloudflare/workers-types@4.20260414.1': {} + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -2145,94 +3972,378 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.2.0': dependencies: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.27.0': optional: true + '@esbuild/aix-ppc64@0.27.3': + optional: true + '@esbuild/android-arm64@0.27.0': optional: true + '@esbuild/android-arm64@0.27.3': + optional: true + '@esbuild/android-arm@0.27.0': optional: true + '@esbuild/android-arm@0.27.3': + optional: true + '@esbuild/android-x64@0.27.0': optional: true + '@esbuild/android-x64@0.27.3': + optional: true + '@esbuild/darwin-arm64@0.27.0': optional: true + '@esbuild/darwin-arm64@0.27.3': + optional: true + '@esbuild/darwin-x64@0.27.0': optional: true + '@esbuild/darwin-x64@0.27.3': + optional: true + '@esbuild/freebsd-arm64@0.27.0': optional: true + '@esbuild/freebsd-arm64@0.27.3': + optional: true + '@esbuild/freebsd-x64@0.27.0': optional: true + '@esbuild/freebsd-x64@0.27.3': + optional: true + '@esbuild/linux-arm64@0.27.0': optional: true + '@esbuild/linux-arm64@0.27.3': + optional: true + '@esbuild/linux-arm@0.27.0': optional: true + '@esbuild/linux-arm@0.27.3': + optional: true + '@esbuild/linux-ia32@0.27.0': optional: true + '@esbuild/linux-ia32@0.27.3': + optional: true + '@esbuild/linux-loong64@0.27.0': optional: true + '@esbuild/linux-loong64@0.27.3': + optional: true + '@esbuild/linux-mips64el@0.27.0': optional: true + '@esbuild/linux-mips64el@0.27.3': + optional: true + '@esbuild/linux-ppc64@0.27.0': optional: true + '@esbuild/linux-ppc64@0.27.3': + optional: true + '@esbuild/linux-riscv64@0.27.0': optional: true + '@esbuild/linux-riscv64@0.27.3': + optional: true + '@esbuild/linux-s390x@0.27.0': optional: true + '@esbuild/linux-s390x@0.27.3': + optional: true + '@esbuild/linux-x64@0.27.0': optional: true + '@esbuild/linux-x64@0.27.3': + optional: true + '@esbuild/netbsd-arm64@0.27.0': optional: true + '@esbuild/netbsd-arm64@0.27.3': + optional: true + '@esbuild/netbsd-x64@0.27.0': optional: true + '@esbuild/netbsd-x64@0.27.3': + optional: true + '@esbuild/openbsd-arm64@0.27.0': optional: true + '@esbuild/openbsd-arm64@0.27.3': + optional: true + '@esbuild/openbsd-x64@0.27.0': optional: true + '@esbuild/openbsd-x64@0.27.3': + optional: true + '@esbuild/openharmony-arm64@0.27.0': optional: true + '@esbuild/openharmony-arm64@0.27.3': + optional: true + '@esbuild/sunos-x64@0.27.0': optional: true + '@esbuild/sunos-x64@0.27.3': + optional: true + '@esbuild/win32-arm64@0.27.0': optional: true + '@esbuild/win32-arm64@0.27.3': + optional: true + '@esbuild/win32-ia32@0.27.0': optional: true + '@esbuild/win32-ia32@0.27.3': + optional: true + '@esbuild/win32-x64@0.27.0': optional: true + '@esbuild/win32-x64@0.27.3': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.2.0(jiti@2.6.1))': + dependencies: + eslint: 10.2.0(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/compat@2.0.5(eslint@10.2.0(jiti@2.6.1))': + dependencies: + '@eslint/core': 1.2.1 + optionalDependencies: + eslint: 10.2.0(jiti@2.6.1) + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.5.5': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.2.0(jiti@2.6.1))': + optionalDependencies: + eslint: 10.2.0(jiti@2.6.1) + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.1': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + + '@fontsource-variable/geist-mono@5.2.7': {} + + '@fontsource-variable/geist@5.2.8': {} + + '@foxui/core@0.8.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': + dependencies: + '@fontsource-variable/geist': 5.2.8 + '@fontsource-variable/geist-mono': 5.2.7 + '@number-flow/svelte': 0.4.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + bits-ui: 2.17.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + clsx: 2.1.1 + dompurify: 3.4.0 + mode-watcher: 1.1.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + svelte-sonner: 1.1.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + tailwind-merge: 3.5.0 + tailwind-variants: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.2) + tailwindcss: 4.2.2 + web-haptics: 0.0.6(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + transitivePeerDependencies: + - '@internationalized/date' + - '@sveltejs/kit' + - react + - react-dom + - vue + + '@foxui/core@0.9.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': + dependencies: + '@fontsource-variable/geist': 5.2.8 + '@fontsource-variable/geist-mono': 5.2.7 + '@jis3r/icons': 2.7.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + '@lucide/svelte': 1.8.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + '@number-flow/svelte': 0.4.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + bits-ui: 2.17.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + clsx: 2.1.1 + dompurify: 3.4.0 + mode-watcher: 1.1.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + svelte-sonner: 1.1.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + tailwind-merge: 3.5.0 + tailwind-variants: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.2) + tailwindcss: 4.2.2 + web-haptics: 0.0.6(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + transitivePeerDependencies: + - '@internationalized/date' + - '@sveltejs/kit' + - react + - react-dom + - vue + + '@foxui/social@0.8.6(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': + dependencies: + '@atcute/bluesky': 3.3.2 + '@atcute/bluesky-richtext-segmenter': 3.0.0 + '@foxui/core': 0.9.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) + '@foxui/text': 0.8.4(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) + '@foxui/time': 0.8.4(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) + '@use-gesture/vanilla': 10.3.1 + bits-ui: 2.17.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + emoji-picker-element: 1.29.1 + hls.js: 1.6.16 + is-emoji-supported: 0.0.5 + plyr: 3.8.4 + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + tailwindcss: 4.2.2 + transitivePeerDependencies: + - '@internationalized/date' + - '@sveltejs/kit' + - '@tiptap/extension-code-block' + - '@tiptap/extensions' + - highlight.js + - react + - react-dom + - vue + + '@foxui/text@0.8.4(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(highlight.js@11.11.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@foxui/core': 0.9.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/extension-bubble-menu': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/extension-code-block-lowlight': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)(highlight.js@11.11.1)(lowlight@3.3.0) + '@tiptap/extension-document': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-dropcursor': 3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) + '@tiptap/extension-floating-menu': 3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/extension-history': 3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) + '@tiptap/extension-image': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-link': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/extension-paragraph': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-placeholder': 3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) + '@tiptap/extension-strike': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-text': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-typography': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-underline': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/markdown': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + '@tiptap/starter-kit': 3.22.3 + '@tiptap/suggestion': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + bits-ui: 2.17.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + lowlight: 3.3.0 + state: link:@tiptap/pm/state + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + svelte-tiptap: 3.0.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/extension-bubble-menu@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extension-floating-menu@3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + tailwindcss: 4.2.2 + transitivePeerDependencies: + - '@internationalized/date' + - '@sveltejs/kit' + - '@tiptap/extension-code-block' + - '@tiptap/extensions' + - highlight.js + - react + - react-dom + - vue + + '@foxui/time@0.8.4(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2)': + dependencies: + '@foxui/core': 0.9.0(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(tailwindcss@4.2.2) + '@number-flow/svelte': 0.4.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + bits-ui: 2.17.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + tailwindcss: 4.2.2 + transitivePeerDependencies: + - '@internationalized/date' + - '@sveltejs/kit' + - react + - react-dom + - vue + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + '@img/colour@1.0.0': {} '@img/sharp-darwin-arm64@0.34.5': @@ -2336,11 +4447,24 @@ snapshots: optionalDependencies: '@types/node': 25.5.0 + '@internationalized/date@3.12.0': + dependencies: + '@swc/helpers': 0.5.21 + + '@jis3r/icons@2.7.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))': + dependencies: + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -2355,6 +4479,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lucide/svelte@1.8.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))': + dependencies: + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.29.2 @@ -2384,6 +4512,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + '@noble/secp256k1@3.0.0': {} '@nodelib/fs.scandir@2.1.5': @@ -2398,6 +4533,12 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@number-flow/svelte@0.4.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))': + dependencies: + esm-env: 1.2.2 + number-flow: 0.6.0 + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + '@optique/core@0.6.11': {} '@optique/run@0.6.11': @@ -2408,6 +4549,10 @@ snapshots: '@oxc-project/types@0.115.0': {} + '@oxc-project/types@0.124.0': {} + + '@polka/url@1.0.0-next.29': {} + '@poppinss/colors@4.1.6': dependencies: kleur: 4.1.5 @@ -2420,53 +4565,106 @@ snapshots: '@poppinss/exception@1.2.3': {} + '@remirror/core-constants@3.0.0': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.15': + optional: true + '@rolldown/binding-android-arm64@1.0.0-rc.9': optional: true + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + optional: true + '@rolldown/binding-darwin-arm64@1.0.0-rc.9': optional: true + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + optional: true + '@rolldown/binding-darwin-x64@1.0.0-rc.9': optional: true + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + optional: true + '@rolldown/binding-freebsd-x64@1.0.0-rc.9': optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': optional: true + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + optional: true + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': optional: true + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + optional: true + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': optional: true + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + optional: true + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': optional: true + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + optional: true + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': optional: true + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + optional: true + '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': optional: true + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + optional: true + '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': optional: true + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': dependencies: '@napi-rs/wasm-runtime': 1.1.1 optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': optional: true + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + optional: true + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': optional: true + '@rolldown/pluginutils@1.0.0-rc.15': {} + '@rolldown/pluginutils@1.0.0-rc.9': {} '@rollup/rollup-android-arm-eabi@4.60.1': @@ -2550,6 +4748,316 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': + dependencies: + acorn: 8.16.0 + + '@sveltejs/adapter-cloudflare@7.2.8(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(wrangler@4.82.2(@cloudflare/workers-types@4.20260414.1))': + dependencies: + '@cloudflare/workers-types': 4.20260414.1 + '@sveltejs/kit': 2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + worktop: 0.8.0-next.18 + wrangler: 4.82.2(@cloudflare/workers-types@4.20260414.1) + + '@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(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.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + '@types/cookie': 0.6.0 + acorn: 8.16.0 + cookie: 0.6.0 + devalue: 5.7.1 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + set-cookie-parser: 3.1.0 + sirv: 3.0.2 + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + optionalDependencies: + typescript: 6.0.2 + + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(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.4(@typescript-eslint/types@8.58.2) + vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + vitefu: 1.1.3(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + + '@swc/helpers@0.5.21': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/forms@0.5.11(tailwindcss@4.2.2)': + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 4.2.2 + + '@tailwindcss/node@4.2.2': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.1 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.2 + + '@tailwindcss/oxide-android-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide@4.2.2': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-x64': 4.2.2 + '@tailwindcss/oxide-freebsd-x64': 4.2.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-x64-musl': 4.2.2 + '@tailwindcss/oxide-wasm32-wasi': 4.2.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 + + '@tailwindcss/vite@4.2.2(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))': + dependencies: + '@tailwindcss/node': 4.2.2 + '@tailwindcss/oxide': 4.2.2 + tailwindcss: 4.2.2 + vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + + '@tiptap/core@3.22.3(@tiptap/pm@3.22.3)': + dependencies: + '@tiptap/pm': 3.22.3 + + '@tiptap/extension-blockquote@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-bold@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-bubble-menu@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + + '@tiptap/extension-bullet-list@3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/extension-list': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + + '@tiptap/extension-code-block-lowlight@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)(highlight.js@11.11.1)(lowlight@3.3.0)': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/extension-code-block': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + highlight.js: 11.11.1 + lowlight: 3.3.0 + + '@tiptap/extension-code-block@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + + '@tiptap/extension-code@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-document@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-dropcursor@3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/extensions': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + + '@tiptap/extension-floating-menu@3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + + '@tiptap/extension-gapcursor@3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/extensions': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + + '@tiptap/extension-hard-break@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-heading@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-history@3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/extensions': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + + '@tiptap/extension-horizontal-rule@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + + '@tiptap/extension-image@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-italic@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-link@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + linkifyjs: 4.3.2 + + '@tiptap/extension-list-item@3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/extension-list': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + + '@tiptap/extension-list-keymap@3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/extension-list': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + + '@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + + '@tiptap/extension-ordered-list@3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/extension-list': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + + '@tiptap/extension-paragraph@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-placeholder@3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/extensions': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + + '@tiptap/extension-strike@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-text@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-typography@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extension-underline@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + + '@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + + '@tiptap/markdown@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + marked: 17.0.6 + + '@tiptap/pm@3.22.3': + dependencies: + prosemirror-changeset: 2.4.1 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-markdown: 1.13.4 + prosemirror-menu: 1.3.0 + prosemirror-model: 1.25.4 + prosemirror-schema-basic: 1.2.4 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8) + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + '@tiptap/starter-kit@3.22.3': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/extension-blockquote': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-bold': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-bullet-list': 3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) + '@tiptap/extension-code': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-code-block': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/extension-document': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-dropcursor': 3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) + '@tiptap/extension-gapcursor': 3.22.3(@tiptap/extensions@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) + '@tiptap/extension-hard-break': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-heading': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-horizontal-rule': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/extension-italic': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-link': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/extension-list': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/extension-list-item': 3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) + '@tiptap/extension-list-keymap': 3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) + '@tiptap/extension-ordered-list': 3.22.3(@tiptap/extension-list@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)) + '@tiptap/extension-paragraph': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-strike': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-text': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extension-underline': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3)) + '@tiptap/extensions': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + + '@tiptap/suggestion@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)': + dependencies: + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -2560,10 +5068,29 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/cookie@0.6.0': {} + '@types/deep-eql@4.0.2': {} + '@types/esrecurse@4.3.1': {} + '@types/estree@1.0.8': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/json-schema@7.0.15': {} + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@2.0.0': {} + '@types/node@12.20.55': {} '@types/node@25.5.0': @@ -2576,6 +5103,107 @@ snapshots: pg-protocol: 1.13.0 pg-types: 2.2.0 + '@types/trusted-types@2.0.7': {} + + '@types/unist@3.0.3': {} + + '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.2 + eslint: 10.2.0(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.2 + debug: 4.4.3 + eslint: 10.2.0(jiti@2.6.1) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.58.2(typescript@6.0.2)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2) + '@typescript-eslint/types': 8.58.2 + debug: 4.4.3 + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.58.2': + dependencies: + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/visitor-keys': 8.58.2 + + '@typescript-eslint/tsconfig-utils@8.58.2(typescript@6.0.2)': + dependencies: + typescript: 6.0.2 + + '@typescript-eslint/type-utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + debug: 4.4.3 + eslint: 10.2.0(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.58.2': {} + + '@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2)': + dependencies: + '@typescript-eslint/project-service': 8.58.2(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2) + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/visitor-keys': 8.58.2 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.58.2': + dependencies: + '@typescript-eslint/types': 8.58.2 + eslint-visitor-keys: 5.0.1 + + '@use-gesture/core@10.3.1': {} + + '@use-gesture/vanilla@10.3.1': + dependencies: + '@use-gesture/core': 10.3.1 + '@vitest/expect@4.1.0': dependencies: '@standard-schema/spec': 1.1.0 @@ -2617,8 +5245,19 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn@8.16.0: {} + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} @@ -2631,16 +5270,39 @@ snapshots: argparse@2.0.1: {} + aria-query@5.3.1: {} + array-union@2.1.0: {} assertion-error@2.0.1: {} + axobject-query@4.1.0: {} + + balanced-match@4.0.4: {} + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 + bits-ui@2.17.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)): + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/dom': 1.7.6 + '@internationalized/date': 3.12.0 + esm-env: 1.2.2 + runed: 0.35.1(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + tabbable: 6.4.0 + transitivePeerDependencies: + - '@sveltejs/kit' + blake3-wasm@2.1.5: {} + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -2660,6 +5322,8 @@ snapshots: dependencies: readdirp: 4.1.2 + clsx@2.1.1: {} + commander@4.1.1: {} confbox@0.1.8: {} @@ -2668,31 +5332,66 @@ snapshots: convert-source-map@2.0.0: {} + cookie@0.6.0: {} + cookie@1.1.1: {} + core-js@3.49.0: {} + + crelt@1.0.6: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + cssesc@3.0.0: {} + + custom-event-polyfill@1.0.7: {} + debug@4.4.3: dependencies: ms: 2.1.3 + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + dequal@2.0.3: {} + detect-indent@6.1.0: {} detect-libc@2.1.2: {} + devalue@5.7.1: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + dir-glob@3.0.1: dependencies: path-type: 4.0.0 + dompurify@3.4.0: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + emoji-picker-element@1.29.1: {} + + enhanced-resolve@5.20.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.2 + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + entities@4.5.0: {} + error-stack-parser-es@1.0.5: {} es-module-lexer@2.0.0: {} @@ -2726,20 +5425,160 @@ snapshots: '@esbuild/win32-ia32': 0.27.0 '@esbuild/win32-x64': 0.27.0 + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@10.2.0(jiti@2.6.1)): + dependencies: + eslint: 10.2.0(jiti@2.6.1) + + eslint-plugin-svelte@3.17.0(eslint@10.2.0(jiti@2.6.1))(svelte@5.55.4(@typescript-eslint/types@8.58.2)): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) + '@jridgewell/sourcemap-codec': 1.5.5 + eslint: 10.2.0(jiti@2.6.1) + esutils: 2.0.3 + globals: 16.5.0 + known-css-properties: 0.37.0 + postcss: 8.5.8 + postcss-load-config: 3.1.4(postcss@8.5.8) + postcss-safe-parser: 7.0.1(postcss@8.5.8) + semver: 7.7.4 + svelte-eslint-parser: 1.6.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + optionalDependencies: + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + transitivePeerDependencies: + - ts-node + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.2.0(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.5.5 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + esm-env@1.2.2: {} - esprima@4.0.1: {} + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrap@2.2.5(@typescript-eslint/types@8.58.2): + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + optionalDependencies: + '@typescript-eslint/types': 8.58.2 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 + esutils@2.0.3: {} + event-target-polyfill@0.0.4: {} expect-type@1.3.0: {} extendable-error@0.1.7: {} + fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2748,13 +5587,21 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + fastq@1.20.1: dependencies: reusify: 1.1.0 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 fill-range@7.1.1: dependencies: @@ -2765,12 +5612,24 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 mlly: 1.8.2 rollup: 4.60.1 + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -2794,6 +5653,14 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@16.5.0: {} + + globals@17.5.0: {} + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -2805,6 +5672,10 @@ snapshots: graceful-fs@4.2.11: {} + highlight.js@11.11.1: {} + + hls.js@1.6.16: {} + hono@4.12.8: {} human-id@4.1.3: {} @@ -2815,6 +5686,14 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + + inline-style-parser@0.2.7: {} + + is-emoji-supported@0.0.5: {} + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -2823,6 +5702,10 @@ snapshots: is-number@7.0.0: {} + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 @@ -2831,6 +5714,8 @@ snapshots: isexe@2.0.0: {} + jiti@2.6.1: {} + joycon@3.1.1: {} js-yaml@3.14.2: @@ -2842,12 +5727,29 @@ snapshots: dependencies: argparse: 2.0.1 + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + kleur@4.1.5: {} + known-css-properties@0.37.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + lightningcss-android-arm64@1.32.0: optional: true @@ -2897,22 +5799,59 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lilconfig@2.1.0: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + linkifyjs@4.3.2: {} + load-tsconfig@0.2.5: {} + loadjs@4.3.0: {} + + locate-character@3.0.0: {} + locate-path@5.0.0: dependencies: p-locate: 4.1.0 + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + lodash.startcase@4.4.0: {} + lowlight@3.3.0: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + highlight.js: 11.11.1 + + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + marked@17.0.6: {} + + mdurl@2.0.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -2920,6 +5859,8 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + mini-svg-data-uri@1.4.4: {} + miniflare@4.20260205.0: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -2932,6 +5873,22 @@ snapshots: - bufferutil - utf-8-validate + miniflare@4.20260410.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.24.4 + workerd: 1.20260410.1 + ws: 8.18.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + mlly@1.8.2: dependencies: acorn: 8.16.0 @@ -2939,8 +5896,16 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 + mode-watcher@1.1.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)): + dependencies: + runed: 0.25.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + svelte-toolbelt: 0.7.1(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + mri@1.2.0: {} + mrmime@2.0.1: {} + ms@2.1.3: {} mz@2.7.0: @@ -2953,10 +5918,27 @@ snapshots: nanoid@5.1.7: {} + natural-compare@1.4.0: {} + + number-flow@0.6.0: + dependencies: + esm-env: 1.2.2 + object-assign@4.1.1: {} obug@2.1.1: {} + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + orderedmap@2.1.1: {} + outdent@0.5.0: {} p-filter@2.1.0: @@ -2967,10 +5949,18 @@ snapshots: dependencies: p-try: 2.2.0 + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + p-locate@4.1.0: dependencies: p-limit: 2.3.0 + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + p-map@2.1.0: {} p-try@2.2.0: {} @@ -3034,6 +6024,8 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.4: {} + pify@4.0.1: {} pirates@4.0.7: {} @@ -3044,6 +6036,21 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 + plyr@3.8.4: + dependencies: + core-js: 3.49.0 + custom-event-polyfill: 1.0.7 + loadjs: 4.3.0 + rangetouch: 2.0.1 + url-polyfill: 1.1.14 + + postcss-load-config@3.1.4(postcss@8.5.8): + dependencies: + lilconfig: 2.1.0 + yaml: 1.10.3 + optionalDependencies: + postcss: 8.5.8 + postcss-load-config@6.0.1(postcss@8.5.8)(tsx@4.21.0): dependencies: lilconfig: 3.1.3 @@ -3051,6 +6058,19 @@ snapshots: postcss: 8.5.8 tsx: 4.21.0 + postcss-safe-parser@7.0.1(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + + postcss-scss@4.0.9(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss@8.5.8: dependencies: nanoid: 3.3.11 @@ -3067,14 +6087,136 @@ snapshots: dependencies: xtend: 4.0.2 + prelude-ls@1.2.1: {} + + prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2)): + dependencies: + prettier: 3.8.1 + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + + prettier-plugin-tailwindcss@0.7.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2)))(prettier@3.8.1): + dependencies: + prettier: 3.8.1 + optionalDependencies: + prettier-plugin-svelte: 3.5.1(prettier@3.8.1)(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + prettier@2.8.8: {} prettier@3.8.1: {} + prosemirror-changeset@2.4.1: + dependencies: + prosemirror-transform: 1.12.0 + + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.4 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.8 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-markdown@1.13.4: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.1 + prosemirror-model: 1.25.4 + + prosemirror-menu@1.3.0: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.7.1 + prosemirror-history: 1.5.0 + prosemirror-state: 1.4.4 + + prosemirror-model@1.25.4: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-basic@1.2.4: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.8 + + prosemirror-transform@1.12.0: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-view@1.41.8: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + punycode.js@2.3.1: {} + + punycode@2.3.1: {} + quansync@0.2.11: {} queue-microtask@1.2.3: {} + rangetouch@2.0.1: {} + read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -3084,12 +6226,35 @@ snapshots: readdirp@4.1.2: {} + regexparam@3.0.0: {} + resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} reusify@1.1.0: {} + rolldown@1.0.0-rc.15: + dependencies: + '@oxc-project/types': 0.124.0 + '@rolldown/pluginutils': 1.0.0-rc.15 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-x64': 1.0.0-rc.15 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 + rolldown@1.0.0-rc.9: dependencies: '@oxc-project/types': 0.115.0 @@ -3142,14 +6307,46 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.1 fsevents: 2.3.3 + rope-sequence@1.3.4: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 + runed@0.23.4(svelte@5.55.4(@typescript-eslint/types@8.58.2)): + dependencies: + esm-env: 1.2.2 + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + + runed@0.25.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)): + dependencies: + esm-env: 1.2.2 + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + + runed@0.28.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)): + dependencies: + esm-env: 1.2.2 + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + + runed@0.35.1(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)): + dependencies: + dequal: 2.0.3 + esm-env: 1.2.2 + lz-string: 1.5.0 + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + optionalDependencies: + '@sveltejs/kit': 2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)) + + sade@1.8.1: + dependencies: + mri: 1.2.0 + safer-buffer@2.1.2: {} semver@7.7.4: {} + set-cookie-parser@3.1.0: {} + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -3191,6 +6388,12 @@ snapshots: signal-exit@4.1.0: {} + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + slash@3.0.0: {} source-map-js@1.2.1: {} @@ -3216,6 +6419,10 @@ snapshots: strip-bom@3.0.0: {} + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -3228,6 +6435,95 @@ snapshots: supports-color@10.2.2: {} + svelte-check@4.4.6(picomatch@4.0.4)(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + typescript: 6.0.2 + transitivePeerDependencies: + - picomatch + + svelte-eslint-parser@1.6.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)): + dependencies: + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + postcss: 8.5.8 + postcss-scss: 4.0.9(postcss@8.5.8) + postcss-selector-parser: 7.1.1 + semver: 7.7.4 + optionalDependencies: + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + + svelte-sonner@1.1.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)): + dependencies: + runed: 0.28.0(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + + svelte-tiptap@3.0.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/extension-bubble-menu@3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/extension-floating-menu@3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3)(svelte@5.55.4(@typescript-eslint/types@8.58.2)): + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.22.3(@tiptap/pm@3.22.3) + '@tiptap/extension-bubble-menu': 3.22.3(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/extension-floating-menu': 3.22.3(@floating-ui/dom@1.7.6)(@tiptap/core@3.22.3(@tiptap/pm@3.22.3))(@tiptap/pm@3.22.3) + '@tiptap/pm': 3.22.3 + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + + svelte-toolbelt@0.10.6(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)): + dependencies: + clsx: 2.1.1 + runed: 0.35.1(@sveltejs/kit@2.57.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.4(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2))(typescript@6.0.2)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + style-to-object: 1.0.14 + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + transitivePeerDependencies: + - '@sveltejs/kit' + + svelte-toolbelt@0.7.1(svelte@5.55.4(@typescript-eslint/types@8.58.2)): + dependencies: + clsx: 2.1.1 + runed: 0.23.4(svelte@5.55.4(@typescript-eslint/types@8.58.2)) + style-to-object: 1.0.14 + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + + svelte@5.55.4(@typescript-eslint/types@8.58.2): + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) + '@types/estree': 1.0.8 + '@types/trusted-types': 2.0.7 + acorn: 8.16.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.7.1 + esm-env: 1.2.2 + esrap: 2.2.5(@typescript-eslint/types@8.58.2) + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' + + tabbable@6.4.0: {} + + tailwind-merge@3.5.0: {} + + tailwind-variants@3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.2): + dependencies: + tailwindcss: 4.2.2 + optionalDependencies: + tailwind-merge: 3.5.0 + + tailwindcss@4.2.2: {} + + tapable@2.3.2: {} + term-size@2.2.1: {} thenify-all@1.6.0: @@ -3246,8 +6542,8 @@ snapshots: tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tinyrainbow@3.1.0: {} @@ -3255,12 +6551,17 @@ snapshots: dependencies: is-number: 7.0.0 + totalist@3.0.1: {} + tree-kill@1.2.2: {} + ts-api-utils@2.5.0(typescript@6.0.2): + dependencies: + typescript: 6.0.2 + ts-interface-checker@0.1.13: {} - tslib@2.8.1: - optional: true + tslib@2.8.1: {} tsup@8.5.1(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3): dependencies: @@ -3297,16 +6598,37 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + type-fest@4.41.0: {} + typescript-eslint@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2): + dependencies: + '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/parser': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} + typescript@6.0.2: {} + + uc.micro@2.1.0: {} + ufo@1.6.3: {} undici-types@7.18.2: {} undici@7.18.2: {} + undici@7.24.4: {} + unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 @@ -3315,6 +6637,18 @@ snapshots: universalify@0.1.2: {} + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-polyfill@1.1.14: {} + + util-deprecate@1.0.2: {} + + valibot@1.3.1(typescript@6.0.2): + optionalDependencies: + typescript: 6.0.2 + vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.0)(tsx@4.21.0): dependencies: '@oxc-project/runtime': 0.115.0 @@ -3329,6 +6663,24 @@ snapshots: fsevents: 2.3.3 tsx: 4.21.0 + vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.8 + rolldown: 1.0.0-rc.15 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.5.0 + esbuild: 0.27.3 + fsevents: 2.3.3 + jiti: 2.6.1 + tsx: 4.21.0 + + vitefu@1.1.3(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)): + optionalDependencies: + vite: 8.0.8(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0) + vitest@4.1.0(@types/node@25.5.0)(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.0)(tsx@4.21.0)): dependencies: '@vitest/expect': 4.1.0 @@ -3356,6 +6708,12 @@ snapshots: transitivePeerDependencies: - msw + w3c-keyname@2.2.8: {} + + web-haptics@0.0.6(svelte@5.55.4(@typescript-eslint/types@8.58.2)): + optionalDependencies: + svelte: 5.55.4(@typescript-eslint/types@8.58.2) + which@2.0.2: dependencies: isexe: 2.0.0 @@ -3365,6 +6723,8 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + word-wrap@1.2.5: {} + workerd@1.20260205.0: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20260205.0 @@ -3373,6 +6733,19 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260205.0 '@cloudflare/workerd-windows-64': 1.20260205.0 + workerd@1.20260410.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260410.1 + '@cloudflare/workerd-darwin-arm64': 1.20260410.1 + '@cloudflare/workerd-linux-64': 1.20260410.1 + '@cloudflare/workerd-linux-arm64': 1.20260410.1 + '@cloudflare/workerd-windows-64': 1.20260410.1 + + worktop@0.8.0-next.18: + dependencies: + mrmime: 2.0.1 + regexparam: 3.0.0 + wrangler@4.63.0(@cloudflare/workers-types@4.20260205.0): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 @@ -3390,10 +6763,31 @@ snapshots: - bufferutil - utf-8-validate + wrangler@4.82.2(@cloudflare/workers-types@4.20260414.1): + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260410.1) + blake3-wasm: 2.1.5 + esbuild: 0.27.3 + miniflare: 4.20260410.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260410.1 + optionalDependencies: + '@cloudflare/workers-types': 4.20260414.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + ws@8.18.0: {} xtend@4.0.2: {} + yaml@1.10.3: {} + + yocto-queue@0.1.0: {} + yocto-queue@1.2.2: {} youch-core@0.3.3: @@ -3408,3 +6802,5 @@ snapshots: '@speed-highlight/core': 1.2.14 cookie: 1.1.1 youch-core: 0.3.3 + + zimmerframe@1.1.4: {} diff --git a/scripts/publish-lexicons.ts b/scripts/publish-lexicons.ts new file mode 100644 index 0000000..d497f2d --- /dev/null +++ b/scripts/publish-lexicons.ts @@ -0,0 +1,42 @@ +/** + * CLI wrapper for `publishLexicons` — publishes contrail's own generated + * lexicons (under `lexicons-generated/`) to the logged-in account's PDS. + * + * Usage: + * LEXICON_ACCOUNT_IDENTIFIER=you.bsky.social \ + * LEXICON_ACCOUNT_PASSWORD=xxxx-xxxx-xxxx-xxxx \ + * pnpm publish-lexicons + * + * pnpm publish-lexicons + */ + +import { join } from "node:path"; +import { publishLexicons } from "../src/publish"; + +const ROOT = join(import.meta.dirname, ".."); + +async function main(): Promise { + const identifier = process.argv[2] ?? process.env.LEXICON_ACCOUNT_IDENTIFIER; + const password = process.argv[3] ?? process.env.LEXICON_ACCOUNT_PASSWORD; + + if (!identifier || !password) { + console.error( + "Usage: pnpm publish-lexicons \n" + + " (or set LEXICON_ACCOUNT_IDENTIFIER and LEXICON_ACCOUNT_PASSWORD env vars)\n\n" + + "Generate an app password at https://bsky.app/settings/app-passwords\n" + + "(or equivalent on your PDS)." + ); + process.exit(1); + } + + await publishLexicons({ + generatedDir: join(ROOT, "lexicons-generated"), + identifier, + password, + }); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/lexicons/tools/atmo/space/admin/addMember.json b/spaces-lexicon-templates/admin/addMember.json similarity index 89% rename from lexicons/tools/atmo/space/admin/addMember.json rename to spaces-lexicon-templates/admin/addMember.json index cc671a9..5267981 100644 --- a/lexicons/tools/atmo/space/admin/addMember.json +++ b/spaces-lexicon-templates/admin/addMember.json @@ -13,7 +13,7 @@ "properties": { "spaceUri": { "type": "string", "format": "at-uri" }, "did": { "type": "string", "format": "did" }, - "perms": { "type": "string", "default": "member" } + "perms": { "type": "string", "knownValues": ["read", "write"], "default": "write" } } } }, diff --git a/lexicons/tools/atmo/space/admin/createSpace.json b/spaces-lexicon-templates/admin/createSpace.json similarity index 90% rename from lexicons/tools/atmo/space/admin/createSpace.json rename to spaces-lexicon-templates/admin/createSpace.json index b43d651..fc4d4d0 100644 --- a/lexicons/tools/atmo/space/admin/createSpace.json +++ b/spaces-lexicon-templates/admin/createSpace.json @@ -14,7 +14,6 @@ "key": { "type": "string", "description": "Space key. Auto-generated (TID) if omitted." }, "memberListRef": { "type": "string", "format": "at-uri" }, "appPolicyRef": { "type": "string", "format": "at-uri" }, - "policy": { "type": "unknown", "description": "Map of collection NSID to tools.atmo.space.defs#collectionPolicy" }, "appPolicy": { "type": "ref", "ref": "tools.atmo.space.defs#appPolicy" } } } diff --git a/spaces-lexicon-templates/admin/removeMember.json b/spaces-lexicon-templates/admin/removeMember.json new file mode 100644 index 0000000..8acdef0 --- /dev/null +++ b/spaces-lexicon-templates/admin/removeMember.json @@ -0,0 +1,36 @@ +{ + "lexicon": 1, + "id": "tools.atmo.space.admin.removeMember", + "defs": { + "main": { + "type": "procedure", + "description": "Remove a member from a space. Owner only. Cannot remove the owner.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["spaceUri", "did"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" }, + "did": { "type": "string", "format": "did" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["ok"], + "properties": { + "ok": { "type": "boolean" } + } + } + }, + "errors": [ + { "name": "NotFound" }, + { "name": "Forbidden" }, + { "name": "InvalidRequest" } + ] + } + } +} diff --git a/lexicons/tools/atmo/space/defs.json b/spaces-lexicon-templates/defs.json similarity index 84% rename from lexicons/tools/atmo/space/defs.json rename to spaces-lexicon-templates/defs.json index 1a5aa2b..736d4de 100644 --- a/lexicons/tools/atmo/space/defs.json +++ b/spaces-lexicon-templates/defs.json @@ -15,7 +15,6 @@ "memberListRef": { "type": "string", "format": "at-uri" }, "appPolicyRef": { "type": "string", "format": "at-uri" }, "createdAt": { "type": "integer" }, - "policy": { "type": "unknown", "description": "Owner-only: map of collection NSID to #collectionPolicy" }, "appPolicy": { "type": "ref", "ref": "#appPolicy", "description": "Owner-only" } } }, @@ -24,7 +23,7 @@ "required": ["did", "perms", "addedAt"], "properties": { "did": { "type": "string", "format": "did" }, - "perms": { "type": "string" }, + "perms": { "type": "string", "knownValues": ["read", "write"], "description": "'write' implies 'read'. Space owner is always implicit write." }, "addedAt": { "type": "integer" }, "addedBy": { "type": "string", "format": "did" } } @@ -42,14 +41,6 @@ "createdAt": { "type": "integer" } } }, - "collectionPolicy": { - "type": "object", - "required": ["read", "write"], - "properties": { - "read": { "type": "string", "knownValues": ["member", "member-own", "owner"] }, - "write": { "type": "string", "knownValues": ["member", "owner"] } - } - }, "appPolicy": { "type": "object", "required": ["mode", "apps"], @@ -64,7 +55,7 @@ "properties": { "tokenHash": { "type": "string" }, "spaceUri": { "type": "string", "format": "at-uri" }, - "perms": { "type": "string" }, + "perms": { "type": "string", "knownValues": ["read", "write"] }, "expiresAt": { "type": "integer" }, "maxUses": { "type": "integer" }, "usedCount": { "type": "integer" }, diff --git a/spaces-lexicon-templates/deleteRecord.json b/spaces-lexicon-templates/deleteRecord.json new file mode 100644 index 0000000..dadc5d2 --- /dev/null +++ b/spaces-lexicon-templates/deleteRecord.json @@ -0,0 +1,34 @@ +{ + "lexicon": 1, + "id": "tools.atmo.space.deleteRecord", + "defs": { + "main": { + "type": "procedure", + "description": "Delete a record from a space. Callers can delete their own records; the space owner can delete any record (via a separate admin path).", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["spaceUri", "collection", "rkey"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" }, + "collection": { "type": "string", "format": "nsid" }, + "rkey": { "type": "string" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["ok"], + "properties": { "ok": { "type": "boolean" } } + } + }, + "errors": [ + { "name": "NotFound" }, + { "name": "Forbidden" } + ] + } + } +} diff --git a/lexicons/tools/atmo/space/getRecord.json b/spaces-lexicon-templates/getRecord.json similarity index 100% rename from lexicons/tools/atmo/space/getRecord.json rename to spaces-lexicon-templates/getRecord.json diff --git a/lexicons/tools/atmo/space/getSpace.json b/spaces-lexicon-templates/getSpace.json similarity index 100% rename from lexicons/tools/atmo/space/getSpace.json rename to spaces-lexicon-templates/getSpace.json diff --git a/lexicons/tools/atmo/space/invite/create.json b/spaces-lexicon-templates/invite/create.json similarity index 92% rename from lexicons/tools/atmo/space/invite/create.json rename to spaces-lexicon-templates/invite/create.json index 7fb8aee..d5c9ad8 100644 --- a/lexicons/tools/atmo/space/invite/create.json +++ b/spaces-lexicon-templates/invite/create.json @@ -12,7 +12,7 @@ "required": ["spaceUri"], "properties": { "spaceUri": { "type": "string", "format": "at-uri" }, - "perms": { "type": "string", "default": "member" }, + "perms": { "type": "string", "knownValues": ["read", "write"], "default": "write" }, "expiresAt": { "type": "integer", "description": "Unix ms timestamp. Omit for no expiry." }, "maxUses": { "type": "integer", "minimum": 1, "description": "Omit for unlimited uses." }, "note": { "type": "string", "maxLength": 500 } diff --git a/lexicons/tools/atmo/space/invite/list.json b/spaces-lexicon-templates/invite/list.json similarity index 100% rename from lexicons/tools/atmo/space/invite/list.json rename to spaces-lexicon-templates/invite/list.json diff --git a/lexicons/tools/atmo/space/invite/redeem.json b/spaces-lexicon-templates/invite/redeem.json similarity index 91% rename from lexicons/tools/atmo/space/invite/redeem.json rename to spaces-lexicon-templates/invite/redeem.json index 7994f68..d3369fa 100644 --- a/lexicons/tools/atmo/space/invite/redeem.json +++ b/spaces-lexicon-templates/invite/redeem.json @@ -22,7 +22,7 @@ "required": ["spaceUri", "perms"], "properties": { "spaceUri": { "type": "string", "format": "at-uri" }, - "perms": { "type": "string" } + "perms": { "type": "string", "knownValues": ["read", "write"] } } } }, diff --git a/lexicons/tools/atmo/space/invite/revoke.json b/spaces-lexicon-templates/invite/revoke.json similarity index 100% rename from lexicons/tools/atmo/space/invite/revoke.json rename to spaces-lexicon-templates/invite/revoke.json diff --git a/spaces-lexicon-templates/listMembers.json b/spaces-lexicon-templates/listMembers.json new file mode 100644 index 0000000..288a2b7 --- /dev/null +++ b/spaces-lexicon-templates/listMembers.json @@ -0,0 +1,34 @@ +{ + "lexicon": 1, + "id": "tools.atmo.space.listMembers", + "defs": { + "main": { + "type": "query", + "description": "List members of a space. Caller must be a member or the owner.", + "parameters": { + "type": "params", + "required": ["spaceUri"], + "properties": { + "spaceUri": { "type": "string", "format": "at-uri" } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["members"], + "properties": { + "members": { + "type": "array", + "items": { "type": "ref", "ref": "tools.atmo.space.defs#memberView" } + } + } + } + }, + "errors": [ + { "name": "NotFound" }, + { "name": "Forbidden" } + ] + } + } +} diff --git a/lexicons/tools/atmo/space/listRecords.json b/spaces-lexicon-templates/listRecords.json similarity index 100% rename from lexicons/tools/atmo/space/listRecords.json rename to spaces-lexicon-templates/listRecords.json diff --git a/spaces-lexicon-templates/listSpaces.json b/spaces-lexicon-templates/listSpaces.json new file mode 100644 index 0000000..bc7a306 --- /dev/null +++ b/spaces-lexicon-templates/listSpaces.json @@ -0,0 +1,33 @@ +{ + "lexicon": 1, + "id": "tools.atmo.space.listSpaces", + "defs": { + "main": { + "type": "query", + "description": "List spaces the caller has access to. Default scope is 'member' (spaces the caller is a member of, including owned); 'owner' lists only spaces the caller owns.", + "parameters": { + "type": "params", + "properties": { + "scope": { "type": "string", "knownValues": ["member", "owner"], "default": "member" }, + "type": { "type": "string", "format": "nsid" }, + "cursor": { "type": "string" }, + "limit": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["spaces"], + "properties": { + "spaces": { + "type": "array", + "items": { "type": "ref", "ref": "tools.atmo.space.defs#spaceView" } + }, + "cursor": { "type": "string" } + } + } + } + } + } +} diff --git a/lexicons/tools/atmo/space/putRecord.json b/spaces-lexicon-templates/putRecord.json similarity index 100% rename from lexicons/tools/atmo/space/putRecord.json rename to spaces-lexicon-templates/putRecord.json diff --git a/src/core/backfill.ts b/src/core/backfill.ts index 2514943..82c584b 100644 --- a/src/core/backfill.ts +++ b/src/core/backfill.ts @@ -3,7 +3,7 @@ import { isDid, isNsid } from "@atcute/lexicons/syntax"; import type { Client } from "@atcute/client"; import type { ContrailConfig, Database, IngestEvent } from "./types"; -import { getDiscoverableCollections, getDependentCollections, DEFAULT_RELAYS } from "./types"; +import { getDiscoverableNsids, getDependentNsids, DEFAULT_RELAYS } from "./types"; import { applyEvents } from "./db"; import { getClient, getPDS } from "./client"; @@ -462,7 +462,7 @@ export async function discoverDIDs( config: ContrailConfig, deadline: number ): Promise { - const collections = getDiscoverableCollections(config); + const collections = getDiscoverableNsids(config); const relays = config.relays ?? DEFAULT_RELAYS; if (relays.length === 0 || collections.length === 0) return []; @@ -498,7 +498,7 @@ export async function discoverDIDs( await insertDiscoveredDIDs(db, dids, collection); discovered.push(...dids); - for (const depCollection of getDependentCollections(config)) { + for (const depCollection of getDependentNsids(config)) { await insertDiscoveredDIDs(db, dids, depCollection); } diff --git a/src/core/db/index.ts b/src/core/db/index.ts index 7c07f0a..2469f31 100644 --- a/src/core/db/index.ts +++ b/src/core/db/index.ts @@ -1,4 +1,4 @@ export { initSchema } from "./schema"; -export { getLastCursor, saveCursor, applyEvents, lookupExistingRecords, queryRecords, pruneFeedItems } from "./records"; +export { getLastCursor, saveCursor, applyEvents, lookupExistingRecords, queryRecords, queryAcrossSources, pruneFeedItems } from "./records"; export type { QueryOptions, SortOption, ExistingRecordInfo } from "./records"; export type { RecordSource } from "../types"; diff --git a/src/core/db/records.ts b/src/core/db/records.ts index 91c9e6f..6bf0a18 100644 --- a/src/core/db/records.ts +++ b/src/core/db/records.ts @@ -8,26 +8,38 @@ import type { RecordRow, RecordSource, } from "../types"; -import { getNestedValue, getRelationField, countColumnName, getFeedFollowCollections, recordsTableName } from "../types"; +import { + getNestedValue, + getRelationField, + countColumnName, + groupedCountColumnName, + getFeedFollowShortNames, + recordsTableName, + spacesRecordsTableName, + shortNameForNsid, + nsidForShortName, +} from "../types"; import { getSearchableFields, ftsTableName, buildFtsContent } from "../search"; import { ftsQueryClause, getDialect } from "../dialect"; // --- Counts --- interface InboundRelation { + /** Short name of the parent collection. */ parentCollection: string; relationName: string; rel: RelationConfig; } +/** Find relations that target the given short-named child collection. */ function getInboundRelations( config: ContrailConfig, - foreignCollection: string + childShortName: string ): InboundRelation[] { const results: InboundRelation[] = []; for (const [colName, colConfig] of Object.entries(config.collections)) { for (const [relName, rel] of Object.entries(colConfig.relations ?? {})) { - if (rel.collection === foreignCollection) { + if (rel.collection === childShortName) { results.push({ parentCollection: colName, relationName: relName, rel }); } } @@ -46,7 +58,9 @@ function collectCountTargets( existingRecordJson: string | null, targets: Map ): void { - const inbound = getInboundRelations(config, event.collection); + const childShort = shortNameForNsid(config, event.collection); + if (!childShort) return; + const inbound = getInboundRelations(config, childShort); if (inbound.length === 0) return; const record = event.record ? JSON.parse(event.record) : null; @@ -108,12 +122,13 @@ function buildBatchCountStatements( ); setBindings.push(targetValue); - // Grouped counts + // Grouped counts — column names are `count__`; match + // against the group's full token value in the record. if (rel.groupBy) { const mapping = (config as ResolvedContrailConfig)._resolved?.relations[parentCollection]?.[relationName]; if (mapping?.groups) { - for (const [, fullToken] of Object.entries(mapping.groups)) { - const groupCol = countColumnName(fullToken); + for (const [groupKey, fullToken] of Object.entries(mapping.groups)) { + const groupCol = groupedCountColumnName(rel.collection, groupKey); setClauses.push( `${groupCol} = (SELECT ${countExpr} FROM ${childTable} WHERE ${getDialect(db).jsonExtract('record', field)} = ? AND ${getDialect(db).jsonExtract('record', rel.groupBy)} = ?)` ); @@ -147,13 +162,15 @@ function buildFtsStatements( // PostgreSQL: tsvector generated column is auto-maintained, no manual FTS sync if (getDialect(db).ftsStrategy === "generated-column") return []; - const colConfig = config.collections[event.collection]; + const short = shortNameForNsid(config, event.collection); + if (!short) return []; + const colConfig = config.collections[short]; if (!colConfig) return []; - const fields = getSearchableFields(event.collection, colConfig); + const fields = getSearchableFields(short, colConfig); if (!fields || fields.length === 0) return []; - const table = ftsTableName(event.collection); + const table = ftsTableName(short); const stmts: Statement[] = []; if (event.operation === "delete") { @@ -189,11 +206,14 @@ function buildFeedStatements( const stmts: Statement[] = []; + const eventShort = shortNameForNsid(config, event.collection); + if (!eventShort) return []; + for (const [, feedConfig] of Object.entries(config.feeds)) { const followTable = recordsTableName(feedConfig.follow); // Target collection: fan out to followers - if (feedConfig.targets.includes(event.collection)) { + if (feedConfig.targets.includes(eventShort)) { if (event.operation === "create" || event.operation === "update") { stmts.push( db @@ -215,13 +235,14 @@ function buildFeedStatements( } // Follow collection: handle follow/unfollow - if (event.collection === feedConfig.follow) { + if (eventShort === feedConfig.follow) { if (event.operation === "create") { const record = event.record ? JSON.parse(event.record) : null; const subject = record?.subject; if (subject) { - for (const targetCol of feedConfig.targets) { - const targetTable = recordsTableName(targetCol); + for (const targetShort of feedConfig.targets) { + const targetTable = recordsTableName(targetShort); + const targetNsid = nsidForShortName(config, targetShort) ?? targetShort; stmts.push( db .prepare( @@ -234,7 +255,7 @@ function buildFeedStatements( LIMIT 100` ) ) - .bind(event.did, targetCol, subject) + .bind(event.did, targetNsid, subject) ); } } @@ -244,8 +265,8 @@ function buildFeedStatements( const parsed = JSON.parse(existingRecord); const subject = parsed?.subject; if (subject) { - for (const targetCol of feedConfig.targets) { - const targetTable = recordsTableName(targetCol); + for (const targetShort of feedConfig.targets) { + const targetTable = recordsTableName(targetShort); stmts.push( db .prepare( @@ -321,21 +342,25 @@ export interface ExistingRecordInfo { export async function lookupExistingRecords( db: Database, events: { uri: string; collection: string }[], - includeRecord: boolean = true + includeRecord: boolean = true, + config?: ContrailConfig ): Promise> { const result = new Map(); if (events.length === 0) return result; - const byCollection = new Map(); + // Group by short name (config lookup); skip events for collections not in our config. + const byShort = new Map(); for (const e of events) { - const uris = byCollection.get(e.collection) ?? []; + const short = config ? shortNameForNsid(config, e.collection) : e.collection; + if (!short) continue; + const uris = byShort.get(short) ?? []; uris.push(e.uri); - byCollection.set(e.collection, uris); + byShort.set(short, uris); } const selectCols = includeRecord ? "uri, cid, record" : "uri, cid"; - for (const [collection, uris] of byCollection) { - const table = recordsTableName(collection); + for (const [short, uris] of byShort) { + const table = recordsTableName(short); for (let i = 0; i < uris.length; i += 50) { const chunk = uris.slice(i, i + 50); const placeholders = chunk.map(() => "?").join(","); @@ -370,7 +395,7 @@ export async function applyEvents( ): Promise { if (events.length === 0) return; - const followCollections = config ? getFeedFollowCollections(config) : []; + const followCollections = config ? getFeedFollowShortNames(config) : []; const hasCountingRelations = config ? Object.values(config.collections).some(c => Object.values(c.relations ?? {}).some(r => r.count !== false) ) : false; @@ -381,7 +406,7 @@ export async function applyEvents( if (options?.existing) { existingMap = options.existing; } else if (config && !options?.skipReplayDetection) { - existingMap = await lookupExistingRecords(db, events, needRecordContent); + existingMap = await lookupExistingRecords(db, events, needRecordContent, config); } else { existingMap = new Map(); } @@ -398,7 +423,13 @@ export async function applyEvents( const countTargets = new Map(); for (const e of events) { - const table = recordsTableName(e.collection); + // Event's collection is an NSID. Look up the short name from config. + // If no config or not found, treat collection string as-is (for tests that pre-populate tables). + const short = config + ? shortNameForNsid(config, e.collection) ?? (config.collections[e.collection] ? e.collection : null) + : e.collection; + if (!short) continue; // unknown collection — skip silently + const table = recordsTableName(short); if (e.operation === "delete") { batch.push(db.prepare(`DELETE FROM ${table} WHERE uri = ?`).bind(e.uri)); @@ -447,25 +478,49 @@ export async function applyEvents( // --- Count columns --- -function getCountColumns(config: ContrailConfig, collection: string): { type: string; column: string }[] { - const colConfig = config.collections[collection]; +/** Count column descriptor. `type` is the identifier returned in API responses and + * accepted in countFilters — we keep the full record token for grouped counts so + * callers pass e.g. "community.lexicon.calendar.rsvp#going" and filter/hydrate by it. */ +function getCountColumns( + config: ContrailConfig, + shortName: string +): { type: string; column: string }[] { + const colConfig = config.collections[shortName]; if (!colConfig?.relations) return []; const columns: { type: string; column: string }[] = []; - const relMap = (config as ResolvedContrailConfig)._resolved?.relations[collection] ?? {}; + const relMap = (config as ResolvedContrailConfig)._resolved?.relations[shortName] ?? {}; for (const [relName, rel] of Object.entries(colConfig.relations)) { if (rel.count === false) continue; + // Total: identifier is the child's short name; column is `count_`. columns.push({ type: rel.collection, column: countColumnName(rel.collection) }); const mapping = relMap[relName]; if (mapping) { - for (const [, fullToken] of Object.entries(mapping.groups)) { - columns.push({ type: fullToken, column: countColumnName(fullToken) }); + for (const [groupKey, fullToken] of Object.entries(mapping.groups)) { + // Grouped: identifier is the full record token (stable across deployments); + // column is `count__`. + columns.push({ + type: fullToken, + column: groupedCountColumnName(rel.collection, groupKey), + }); } } } return columns; } +/** For a given "count type" (short name or full group token), return the DB column. */ +function countColumnForType( + config: ContrailConfig, + shortName: string, + type: string +): string | null { + for (const col of getCountColumns(config, shortName)) { + if (col.type === type) return col.column; + } + return null; +} + // --- Query --- export interface SortOption { @@ -474,6 +529,36 @@ export interface SortOption { direction: "asc" | "desc"; } +/** Opaque keyset cursor. `t` is the tiebreaker (time_us of the last row), + * `v` is the sort-key value (string for record fields, number for counts), + * `k` identifies the sort so we can reject mismatched cursors. */ +interface CursorPayload { + t: number; + v?: string | number; + k: "time" | string; // "time" | `field:` | `count:` +} + +function sortKind(sort?: SortOption): "time" | string { + if (sort?.recordField) return `field:${sort.recordField}`; + if (sort?.countType) return `count:${sort.countType}`; + return "time"; +} + +function encodeCursor(payload: CursorPayload): string { + return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url"); +} + +function decodeCursor(cursor: string): CursorPayload | null { + try { + const json = Buffer.from(cursor, "base64url").toString("utf8"); + const p = JSON.parse(json); + if (typeof p?.t !== "number" || typeof p?.k !== "string") return null; + return p as CursorPayload; + } catch { + return null; + } +} + export interface QueryOptions { collection: string; did?: string; @@ -485,6 +570,9 @@ export interface QueryOptions { sort?: SortOption; search?: string; source?: RecordSource; + /** When set, query the per-space table (`spaces_records_`) instead of the + * public `records_` table, scoped to rows where `space_uri = ?`. */ + spaceUri?: string; } export async function queryRecords( @@ -493,7 +581,7 @@ export async function queryRecords( options: QueryOptions ): Promise<{ records: (RecordRow & { counts?: Record })[]; cursor?: string }> { const { - collection, + collection: collectionInput, did, limit: rawLimit, cursor, @@ -503,13 +591,25 @@ export async function queryRecords( sort, search, source, + spaceUri, } = options; - const table = recordsTableName(collection); + // Accept either the short name (canonical) or the full NSID for convenience. + const collection = + config.collections[collectionInput] + ? collectionInput + : shortNameForNsid(config, collectionInput) ?? collectionInput; + + const table = spaceUri ? spacesRecordsTableName(collection) : recordsTableName(collection); const limit = Math.min(Math.max(1, rawLimit ?? 50), 200); const conditions: string[] = []; const bindings: (string | number)[] = []; + if (spaceUri) { + conditions.push("r.space_uri = ?"); + bindings.push(spaceUri); + } + if (source?.conditions) conditions.push(...source.conditions); if (source?.params) bindings.push(...source.params); @@ -520,40 +620,28 @@ export async function queryRecords( bindings.push(did); } - // Cursor = AT URI of last seen record. Look it up to get keyset values. + // Opaque keyset cursor encoding { t, v?, k }. Silently ignored if it doesn't + // match the current sort — callers shouldn't mix sort params with stale cursors. + const expectedKind = sortKind(sort); if (cursor) { - // Only select the columns needed for cursor pagination - let cursorSelect: string; - if (sort?.recordField) { - cursorSelect = `time_us, ${getDialect(db).jsonExtract('record', sort.recordField)} as sort_value`; - } else if (sort?.countType) { - const sortCol = countColumnName(sort.countType); - cursorSelect = `time_us, ${sortCol}`; - } else { - cursorSelect = "time_us"; - } - - const cursorRow = await db - .prepare(`SELECT ${cursorSelect} FROM ${table} WHERE uri = ?`) - .bind(cursor) - .first(); - - if (cursorRow) { + const payload = decodeCursor(cursor); + if (payload && payload.k === expectedKind) { if (sort?.recordField) { - const sortValue = cursorRow.sort_value; const sortExpr = getDialect(db).jsonExtract('r.record', sort.recordField); const cmp = sort.direction === "desc" ? "<" : ">"; conditions.push(`(${sortExpr} ${cmp} ? OR (${sortExpr} = ? AND r.time_us < ?))`); - bindings.push(sortValue ?? "", sortValue ?? "", cursorRow.time_us); + const v = payload.v ?? ""; + bindings.push(v as string | number, v as string | number, payload.t); } else if (sort?.countType) { - const sortCol = countColumnName(sort.countType); - const countValue = cursorRow[sortCol] ?? 0; + const sortCol = countColumnForType(config, collection, sort.countType); + if (!sortCol) throw new Error(`Unknown countType: ${sort.countType}`); const cmp = sort.direction === "desc" ? "<" : ">"; conditions.push(`(r.${sortCol} ${cmp} ? OR (r.${sortCol} = ? AND r.time_us < ?))`); - bindings.push(countValue, countValue, cursorRow.time_us); + const v = Number(payload.v ?? 0); + bindings.push(v, v, payload.t); } else { conditions.push("r.time_us < ?"); - bindings.push(cursorRow.time_us); + bindings.push(payload.t); } } } @@ -575,21 +663,24 @@ export async function queryRecords( } for (const [type, minCount] of Object.entries(countFilters)) { - const col = countColumnName(type); + const col = countColumnForType(config, collection, type); + if (!col) continue; // unknown count type — skip filter conditions.push(`r.${col} >= ?`); bindings.push(minCount); } - // FTS search + // FTS search. Not supported in space mode yet (would need composite keying + // because the same at-URI can appear in multiple spaces). let ftsJoin = ""; let ftsClause: ReturnType | null = null; - if (search) { + if (search && !spaceUri) { const colConfig2 = config.collections[collection]; const fields = colConfig2 ? getSearchableFields(collection, colConfig2) : null; if (fields && fields.length > 0) { ftsClause = ftsQueryClause(getDialect(db), recordsTableName(collection)); ftsJoin = ftsClause.join; conditions.push(ftsClause.condition); + // SECURITY: `search` is user input bound as a parameter, not interpolated. bindings.push(search); } } @@ -609,7 +700,8 @@ export async function queryRecords( orderBy = `${getDialect(db).jsonExtract('r.record', sort.recordField)} ${dir}, r.time_us DESC`; } else if (sort?.countType) { const dir = sort.direction === "desc" ? "DESC" : "ASC"; - const sortCol = countColumnName(sort.countType); + const sortCol = countColumnForType(config, collection, sort.countType); + if (!sortCol) throw new Error(`Unknown countType: ${sort.countType}`); orderBy = `r.${sortCol} ${dir}, r.time_us DESC`; } else if (ftsClause) { orderBy = `${ftsClause.orderExpr}, r.time_us DESC`; @@ -630,16 +722,18 @@ export async function queryRecords( .bind(...bindings) .all(); + const nsid = nsidForShortName(config, collection) ?? collection; const records = (result.results ?? []).map((row: any) => { const rec: RecordRow & { counts?: Record } = { uri: row.uri, did: row.did, - collection, + collection: nsid, rkey: row.rkey, cid: row.cid, record: row.record, time_us: row.time_us, indexed_at: row.indexed_at, + ...(spaceUri ? { _space: spaceUri } : {}), }; if (countCols.length > 0) { const counts: Record = {}; @@ -654,11 +748,89 @@ export async function queryRecords( const nextCursor = records.length === limit - ? records[records.length - 1].uri + ? buildCursor(records[records.length - 1], sort, expectedKind) : undefined; return { records, cursor: nextCursor }; } +/** Build an opaque keyset cursor from the last row of a page. */ +function buildCursor( + row: RecordRow & { counts?: Record }, + sort: SortOption | undefined, + kind: string +): string { + const t = Number(row.time_us); + if (sort?.recordField) { + const parsed = row.record ? JSON.parse(row.record) : null; + const v = parsed ? getNestedValue(parsed, sort.recordField) : undefined; + return encodeCursor({ t, v: v == null ? "" : String(v), k: kind }); + } + if (sort?.countType) { + const v = row.counts?.[sort.countType] ?? 0; + return encodeCursor({ t, v, k: kind }); + } + return encodeCursor({ t, k: kind }); +} + +/** Compare two rows according to the active sort order. Returns negative if + * `a` should come before `b`, positive otherwise. Matches the SQL ORDER BY. */ +function compareRows( + a: RecordRow & { counts?: Record }, + b: RecordRow & { counts?: Record }, + sort: SortOption | undefined +): number { + const timeCmp = Number(b.time_us) - Number(a.time_us); // time_us DESC + if (sort?.recordField) { + const ar = a.record ? JSON.parse(a.record) : null; + const br = b.record ? JSON.parse(b.record) : null; + const av = ar ? getNestedValue(ar, sort.recordField) : undefined; + const bv = br ? getNestedValue(br, sort.recordField) : undefined; + const dir = sort.direction === "desc" ? -1 : 1; + const cmp = (av === bv ? 0 : (av! < bv! ? -1 : 1)) * dir; + return cmp !== 0 ? cmp : timeCmp; + } + if (sort?.countType) { + const av = a.counts?.[sort.countType] ?? 0; + const bv = b.counts?.[sort.countType] ?? 0; + const dir = sort.direction === "desc" ? -1 : 1; + const cmp = (av === bv ? 0 : (av < bv ? -1 : 1)) * dir; + return cmp !== 0 ? cmp : timeCmp; + } + return timeCmp; +} + +/** Run a listRecords query across the public table and a set of per-space tables + * in parallel, then merge according to the active sort order. The cursor is a + * shared keyset cursor — every sub-query applies the same `WHERE` keyset, so + * pagination is consistent across sources. */ +export async function queryAcrossSources( + db: Database, + config: ContrailConfig, + options: QueryOptions, + spaceUris: string[] +): Promise<{ records: (RecordRow & { counts?: Record })[]; cursor?: string }> { + if (spaceUris.length === 0) { + return queryRecords(db, config, options); + } + const limit = Math.min(Math.max(1, options.limit ?? 50), 200); + const perSourceLimit = limit; // each source fetches up to `limit`; we trim after merge + + const tasks: Promise<{ records: (RecordRow & { counts?: Record })[] }>[] = [ + queryRecords(db, config, { ...options, limit: perSourceLimit }), + ]; + for (const spaceUri of spaceUris) { + tasks.push(queryRecords(db, config, { ...options, spaceUri, limit: perSourceLimit })); + } + const results = await Promise.all(tasks); + const merged = results.flatMap((r) => r.records); + merged.sort((a, b) => compareRows(a, b, options.sort)); + const trimmed = merged.slice(0, limit); + const kind = sortKind(options.sort); + const cursor = + trimmed.length === limit ? buildCursor(trimmed[trimmed.length - 1], options.sort, kind) : undefined; + return { records: trimmed, cursor }; +} + // --- Users --- diff --git a/src/core/db/schema.ts b/src/core/db/schema.ts index 8ff3fc4..66a0f0c 100644 --- a/src/core/db/schema.ts +++ b/src/core/db/schema.ts @@ -1,9 +1,16 @@ import type { ContrailConfig, Database, ResolvedContrailConfig, ResolvedMaps } from "../types"; import type { SqlDialect } from "../dialect"; import { buildFtsSchema, getDialect } from "../dialect"; -import { getRelationField, countColumnName, recordsTableName, resolveConfig } from "../types"; +import { + getRelationField, + countColumnName, + groupedCountColumnName, + recordsTableName, + spacesRecordsTableName, + resolveConfig, +} from "../types"; import { getSearchableFields } from "../search"; -import { buildSpacesSchema } from "../spaces/schema"; +import { buildSpacesBaseSchema } from "../spaces/schema"; function getResolved(config: ContrailConfig): ResolvedMaps { return (config as ResolvedContrailConfig)._resolved ?? resolveConfig(config)._resolved; @@ -45,45 +52,94 @@ function sanitizeName(name: string): string { return name.replace(/[^a-zA-Z0-9]/g, "_"); } -function buildCollectionTables(config: ContrailConfig, dialect: SqlDialect): string[] { +interface BuilderOpts { + /** Emit tables for the spaces variant (spaces_records_ with space_uri column). */ + forSpaces?: boolean; +} + +function tableFor(shortName: string, opts: BuilderOpts): string { + return opts.forSpaces ? spacesRecordsTableName(shortName) : recordsTableName(shortName); +} + +function namePrefix(opts: BuilderOpts): string { + return opts.forSpaces ? "sp_" : ""; +} + +export function buildCollectionTables( + config: ContrailConfig, + dialect: SqlDialect, + opts: BuilderOpts = {} +): string[] { const stmts: string[] = []; - for (const collection of Object.keys(config.collections)) { - const table = recordsTableName(collection); - stmts.push( - `CREATE TABLE IF NOT EXISTS ${table} ( - uri TEXT PRIMARY KEY, - did TEXT NOT NULL, - rkey TEXT NOT NULL, - cid TEXT, - record ${dialect.recordColumnType}, - time_us ${dialect.bigintType} NOT NULL, - indexed_at ${dialect.bigintType} NOT NULL - )` - ); - stmts.push(`CREATE INDEX IF NOT EXISTS idx_${sanitizeName(collection)}_did ON ${table}(did)`); - stmts.push(`CREATE INDEX IF NOT EXISTS idx_${sanitizeName(collection)}_time ON ${table}(time_us DESC)`); + for (const [shortName, colConfig] of Object.entries(config.collections)) { + if (opts.forSpaces && colConfig.allowInSpaces === false) continue; + const table = tableFor(shortName, opts); + const np = namePrefix(opts); + if (opts.forSpaces) { + stmts.push( + `CREATE TABLE IF NOT EXISTS ${table} ( + space_uri TEXT NOT NULL, + uri TEXT NOT NULL, + did TEXT NOT NULL, + rkey TEXT NOT NULL, + cid TEXT, + record ${dialect.recordColumnType}, + time_us ${dialect.bigintType} NOT NULL, + indexed_at ${dialect.bigintType} NOT NULL, + PRIMARY KEY (space_uri, did, rkey) + )` + ); + stmts.push( + `CREATE INDEX IF NOT EXISTS idx_${np}${sanitizeName(shortName)}_space_time ON ${table}(space_uri, time_us DESC)` + ); + stmts.push( + `CREATE INDEX IF NOT EXISTS idx_${np}${sanitizeName(shortName)}_space_did ON ${table}(space_uri, did)` + ); + } else { + stmts.push( + `CREATE TABLE IF NOT EXISTS ${table} ( + uri TEXT PRIMARY KEY, + did TEXT NOT NULL, + rkey TEXT NOT NULL, + cid TEXT, + record ${dialect.recordColumnType}, + time_us ${dialect.bigintType} NOT NULL, + indexed_at ${dialect.bigintType} NOT NULL + )` + ); + stmts.push(`CREATE INDEX IF NOT EXISTS idx_${sanitizeName(shortName)}_did ON ${table}(did)`); + stmts.push(`CREATE INDEX IF NOT EXISTS idx_${sanitizeName(shortName)}_time ON ${table}(time_us DESC)`); + } } return stmts; } -function buildDynamicIndexes(config: ContrailConfig, dialect: SqlDialect): string[] { +export function buildDynamicIndexes( + config: ContrailConfig, + dialect: SqlDialect, + opts: BuilderOpts = {} +): string[] { const resolved = getResolved(config); const indexes: string[] = []; + const np = namePrefix(opts); for (const [collection, colConfig] of Object.entries(config.collections)) { - const table = recordsTableName(collection); + if (opts.forSpaces && colConfig.allowInSpaces === false) continue; + const table = tableFor(collection, opts); const queryable = resolved.queryable[collection] ?? colConfig.queryable ?? {}; for (const field of Object.keys(queryable)) { - const idxName = `idx_${sanitizeName(collection)}_${sanitizeName(field)}`; + const idxName = `idx_${np}${sanitizeName(collection)}_${sanitizeName(field)}`; indexes.push( `CREATE INDEX IF NOT EXISTS ${idxName} ON ${table}(${dialect.indexExpression(dialect.jsonExtract('record', field))})` ); } - // Relation field indexes go on the CHILD collection's table for (const [, rel] of Object.entries(colConfig.relations ?? {})) { + const childShort = rel.collection; + const childConfig = config.collections[childShort]; + if (opts.forSpaces && childConfig?.allowInSpaces === false) continue; const on = getRelationField(rel); - const childTable = recordsTableName(rel.collection); - const idxName = `idx_${sanitizeName(rel.collection)}_${sanitizeName(on)}`; + const childTable = tableFor(childShort, opts); + const idxName = `idx_${np}${sanitizeName(childShort)}_${sanitizeName(on)}`; indexes.push( `CREATE INDEX IF NOT EXISTS ${idxName} ON ${childTable}(${dialect.indexExpression(dialect.jsonExtract('record', on))})` ); @@ -92,13 +148,15 @@ function buildDynamicIndexes(config: ContrailConfig, dialect: SqlDialect): strin return indexes; } -function buildCountColumns(config: ContrailConfig): string[] { +export function buildCountColumns(config: ContrailConfig, opts: BuilderOpts = {}): string[] { const resolved = getResolved(config); const stmts: string[] = []; - const addedColumns = new Map>(); // table → columns + const addedColumns = new Map>(); + const np = namePrefix(opts); for (const [collection, colConfig] of Object.entries(config.collections)) { - const table = recordsTableName(collection); + if (opts.forSpaces && colConfig.allowInSpaces === false) continue; + const table = tableFor(collection, opts); const relMap = resolved.relations[collection] ?? {}; if (!addedColumns.has(table)) addedColumns.set(table, new Set()); @@ -106,7 +164,7 @@ function buildCountColumns(config: ContrailConfig): string[] { for (const [relName, rel] of Object.entries(colConfig.relations ?? {})) { if (rel.count === false) continue; - // Total count column — on the PARENT collection's table + if (opts.forSpaces && config.collections[rel.collection]?.allowInSpaces === false) continue; const totalCol = countColumnName(rel.collection); if (!tableColumns.has(totalCol)) { tableColumns.add(totalCol); @@ -115,14 +173,13 @@ function buildCountColumns(config: ContrailConfig): string[] { ); } stmts.push( - `CREATE INDEX IF NOT EXISTS idx_${sanitizeName(collection)}_${totalCol} ON ${table}(${totalCol} DESC, time_us DESC)` + `CREATE INDEX IF NOT EXISTS idx_${np}${sanitizeName(collection)}_${totalCol} ON ${table}(${totalCol} DESC, time_us DESC)` ); - // Grouped count columns const mapping = relMap[relName]; if (mapping) { - for (const [, fullToken] of Object.entries(mapping.groups)) { - const groupCol = countColumnName(fullToken); + for (const groupKey of Object.keys(mapping.groups)) { + const groupCol = groupedCountColumnName(rel.collection, groupKey); if (!tableColumns.has(groupCol)) { tableColumns.add(groupCol); stmts.push( @@ -130,7 +187,7 @@ function buildCountColumns(config: ContrailConfig): string[] { ); } stmts.push( - `CREATE INDEX IF NOT EXISTS idx_${sanitizeName(collection)}_${groupCol} ON ${table}(${groupCol} DESC, time_us DESC)` + `CREATE INDEX IF NOT EXISTS idx_${np}${sanitizeName(collection)}_${groupCol} ON ${table}(${groupCol} DESC, time_us DESC)` ); } } @@ -159,7 +216,6 @@ function buildFeedTables(config: ContrailConfig, dialect: SqlDialect): string[] )`, ]; - // Index follow collections on subject for efficient fan-out lookups const followCollections = new Set(Object.values(config.feeds).map((f) => f.follow)); for (const col of followCollections) { const table = recordsTableName(col); @@ -172,12 +228,17 @@ function buildFeedTables(config: ContrailConfig, dialect: SqlDialect): string[] return stmts; } -function buildFtsTables(config: ContrailConfig, dialect: SqlDialect): string[] { +export function buildFtsTables( + config: ContrailConfig, + dialect: SqlDialect, + opts: BuilderOpts = {} +): string[] { const stmts: string[] = []; for (const [collection, colConfig] of Object.entries(config.collections)) { + if (opts.forSpaces && colConfig.allowInSpaces === false) continue; const fields = getSearchableFields(collection, colConfig); if (!fields || fields.length === 0) continue; - const table = recordsTableName(collection); + const table = tableFor(collection, opts); stmts.push(...buildFtsSchema(dialect, table, fields)); } return stmts; @@ -203,6 +264,25 @@ export interface InitSchemaOptions { spacesDb?: Database; } +async function applySpacesSchema( + target: Database, + config: ContrailConfig, + dialect: SqlDialect +): Promise { + const base = buildSpacesBaseSchema(dialect); + const perCollection = buildCollectionTables(config, dialect, { forSpaces: true }); + const indexes = buildDynamicIndexes(config, dialect, { forSpaces: true }); + await target.batch([...base, ...perCollection, ...indexes].map((s) => target.prepare(s))); + + const ftsStmts = buildFtsTables(config, dialect, { forSpaces: true }); + for (const stmt of ftsStmts) { + try { await target.prepare(stmt).run(); } catch { /* FTS5 unavailable */ } + } + for (const stmt of buildCountColumns(config, { forSpaces: true })) { + try { await target.prepare(stmt).run(); } catch { /* already exists */ } + } +} + export async function initSchema( db: Database, config: ContrailConfig, @@ -219,16 +299,13 @@ export async function initSchema( const spacesDb = options.spacesDb; const spacesSharesMainDb = !spacesDb || spacesDb === db; - const inlineSpacesStatements = - config.spaces && spacesSharesMainDb ? buildSpacesSchema(db) : []; - const all = [...baseStatements, ...collectionStatements, ...indexStatements, ...feedStatements, ...inlineSpacesStatements]; + const all = [...baseStatements, ...collectionStatements, ...indexStatements, ...feedStatements]; await db.batch(all.map((s) => db.prepare(s))); - if (config.spaces && spacesDb && !spacesSharesMainDb) { - const spacesStatements = buildSpacesSchema(spacesDb); - await spacesDb.batch(spacesStatements.map((s) => spacesDb.prepare(s))); + if (config.spaces) { + await applySpacesSchema(spacesSharesMainDb ? db : spacesDb!, config, dialect); } // FTS5 may not be available (e.g. node:sqlite) — skip gracefully diff --git a/src/core/jetstream.ts b/src/core/jetstream.ts index 9794c86..88d6485 100644 --- a/src/core/jetstream.ts +++ b/src/core/jetstream.ts @@ -1,6 +1,6 @@ import { JetstreamSubscription } from "@atcute/jetstream"; import type { ContrailConfig, IngestEvent, Database, Logger } from "./types"; -import { getCollectionNames, getDependentCollections, DEFAULT_FEED_MAX_ITEMS } from "./types"; +import { getCollectionNsids, getDependentNsids, DEFAULT_FEED_MAX_ITEMS } from "./types"; import { initSchema, getLastCursor, saveCursor, applyEvents, pruneFeedItems } from "./db"; import { refreshStaleIdentities } from "./identity"; @@ -33,8 +33,8 @@ export async function ingestEvents( const deadline = Date.now() + safetyTimeoutMs; const collected: IngestEvent[] = []; - const collections = getCollectionNames(config); - const dependentCollections = new Set(getDependentCollections(config)); + const collections = getCollectionNsids(config); + const dependentCollections = new Set(getDependentNsids(config)); const urls = config.jetstreams ?? []; const subscription = new JetstreamSubscription({ @@ -116,14 +116,14 @@ export async function runIngestCycle( } const cursor = await getLastCursor(db); - const collections = getCollectionNames(config); + const collections = getCollectionNsids(config); log.log( `Starting ingestion. Cursor: ${cursor ?? "none"}, Collections: ${collections.join(", ")}` ); // Load known DIDs for filtering dependent collections - const dependentCollections = getDependentCollections(config); + const dependentCollections = getDependentNsids(config); let knownDids: Set | undefined; if (dependentCollections.length > 0) { diff --git a/src/core/persistent.ts b/src/core/persistent.ts index f2ffabc..0574656 100644 --- a/src/core/persistent.ts +++ b/src/core/persistent.ts @@ -1,6 +1,6 @@ import type { JetstreamSubscription } from "@atcute/jetstream"; import type { ContrailConfig, IngestEvent, Database, Logger } from "./types"; -import { getCollectionNames, getDependentCollections, DEFAULT_FEED_MAX_ITEMS } from "./types"; +import { getCollectionNsids, getDependentNsids, DEFAULT_FEED_MAX_ITEMS } from "./types"; import { initSchema, getLastCursor, saveCursor, applyEvents, pruneFeedItems } from "./db"; import { refreshStaleIdentities } from "./identity"; import { createIngestState } from "./jetstream"; @@ -39,7 +39,7 @@ export async function runPersistent( } // Load known DIDs for dependent collection filtering - const dependentCollections = new Set(getDependentCollections(config)); + const dependentCollections: Set = new Set(getDependentNsids(config)); let knownDids: Set | undefined; if (dependentCollections.size > 0) { const result = await db @@ -50,7 +50,7 @@ export async function runPersistent( log.log(`Loaded ${knownDids.size} known DIDs from database`); } - const collections = getCollectionNames(config); + const collections = getCollectionNsids(config); let reconnectAttempts = 0; while (!signal?.aborted) { diff --git a/src/core/router/admin.ts b/src/core/router/admin.ts index c6b2e64..a3b754b 100644 --- a/src/core/router/admin.ts +++ b/src/core/router/admin.ts @@ -1,6 +1,6 @@ import type { Hono } from "hono"; import type { ContrailConfig, Database } from "../types"; -import { getCollectionNames, recordsTableName } from "../types"; +import { getCollectionShortNames, recordsTableName, nsidForShortName } from "../types"; import { getLastCursor } from "../db"; export function registerAdminRoutes( @@ -25,13 +25,14 @@ export function registerAdminRoutes( app.get(`/xrpc/${ns}.getOverview`, async (c) => { const collections: { collection: string; records: number; unique_users: number }[] = []; - for (const collection of getCollectionNames(config)) { - const table = recordsTableName(collection); + for (const short of getCollectionShortNames(config)) { + const table = recordsTableName(short); + const nsid = nsidForShortName(config, short) ?? short; const row = await db .prepare(`SELECT COUNT(*) as records, COUNT(DISTINCT did) as unique_users FROM ${table}`) .first<{ records: number; unique_users: number }>(); if (row) { - collections.push({ collection, records: row.records, unique_users: row.unique_users }); + collections.push({ collection: nsid, records: row.records, unique_users: row.unique_users }); } } diff --git a/src/core/router/collection.ts b/src/core/router/collection.ts index 81c47ad..6d08f94 100644 --- a/src/core/router/collection.ts +++ b/src/core/router/collection.ts @@ -1,7 +1,14 @@ -import type { Hono } from "hono"; +import type { Context, Hono } from "hono"; import type { ContrailConfig, ResolvedContrailConfig, Database, RecordRow, QueryableField, RecordSource, RelationConfig } from "../types"; -import { getCollectionNames, countColumnName, recordsTableName } from "../types"; -import { queryRecords } from "../db"; +import { + getCollectionShortNames, + countColumnName, + groupedCountColumnName, + recordsTableName, + nsidForShortName, + getCollectionMethods, +} from "../types"; +import { queryRecords, queryAcrossSources } from "../db"; import type { SortOption } from "../db/records"; import { backfillUser } from "../backfill"; import { resolveHydrates, resolveReferences, parseHydrateParams } from "./hydrate"; @@ -9,13 +16,18 @@ import { resolveProfiles, collectDids } from "./profiles"; import { resolveActor } from "../identity"; import type { FormattedRecord } from "./helpers"; import { formatRecord, parseIntParam, fieldToParam } from "./helpers"; +import { verifyServiceAuthRequest } from "../spaces/auth"; +import { checkAccess } from "../spaces/acl"; +import type { SpacesContext } from "."; +import type { Nsid } from "@atcute/lexicons"; export async function runPipeline( db: Database, config: ContrailConfig, collection: string, params: URLSearchParams, - source?: RecordSource + source?: RecordSource, + spaceUris?: string[] ): Promise<{ records: FormattedRecord[]; cursor?: string; profiles?: any[] }> { const colConfig = config.collections[collection]; if (!colConfig) throw new Error(`Unknown collection: ${collection}`); @@ -37,7 +49,9 @@ export async function runPipeline( if (!resolved) throw new Error("Could not resolve actor"); did = resolved; if (wantBackfill) { - await backfillUser(db, did, collection, Date.now() + 10_000, config); + // backfillUser expects the record NSID (for PDS calls), not the short name. + const nsid = nsidForShortName(config, collection) ?? collection; + await backfillUser(db, did, nsid, Date.now() + 10_000, config); } } @@ -109,8 +123,9 @@ export async function runPipeline( } const search = params.get("search") || undefined; + const spaceUri = params.get("spaceUri") || undefined; - const result = await queryRecords(db, config, { + const queryOpts = { collection, did, limit, @@ -121,7 +136,11 @@ export async function runPipeline( sort, search, source, - }); + spaceUri, + }; + const result = spaceUris && spaceUris.length > 0 && !spaceUri + ? await queryAcrossSources(db, config, queryOpts, spaceUris) + : await queryRecords(db, config, queryOpts); const rows = result.records; const hydrateRequested = parseHydrateParams(params, relations, references); @@ -129,13 +148,15 @@ export async function runPipeline( db, relations, hydrateRequested.relations, - rows + rows, + config ); const refs = await resolveReferences( db, references, hydrateRequested.references, - rows + rows, + config ); const formattedRecords: FormattedRecord[] = rows.map((row) => { @@ -171,28 +192,137 @@ export async function runPipeline( export function registerCollectionRoutes( app: Hono, db: Database, - config: ContrailConfig + config: ContrailConfig, + spacesCtx?: SpacesContext | null ): void { - for (const collection of getCollectionNames(config)) { + const ns = config.namespace; + + /** When a per-collection endpoint receives `?spaceUri=...`, verify the JWT, + * resolve membership, run the space ACL, and return the caller DID if allowed. + * Returns null if the spaces subsystem isn't available; the handler should + * then treat the spaceUri as invalid. + * Throws by returning a Response (caller checks via `instanceof Response`). */ + async function gateSpaceAccess( + c: Context, + spaceUri: string, + op: "read" + ): Promise { + if (!spacesCtx) { + return c.json( + { error: "InvalidRequest", message: "spaces not configured on this service" }, + 501 + ); + } + const nsid = new URL(c.req.url).pathname.match(/\/xrpc\/([^?]+)/)?.[1] as Nsid | null; + const auth = await verifyServiceAuthRequest(spacesCtx.verifier, c.req.raw, nsid); + if (!auth) { + return c.json( + { error: "AuthRequired", message: "spaceUri requires a valid service-auth JWT" }, + 401 + ); + } + const space = await spacesCtx.adapter.getSpace(spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + const member = await spacesCtx.adapter.getMember(spaceUri, auth.issuer); + const result = checkAccess({ + op, + space, + callerDid: auth.issuer, + member, + clientId: auth.clientId, + }); + if (!result.allow) { + return c.json({ error: "Forbidden", reason: result.reason }, 403); + } + return { callerDid: auth.issuer, clientId: auth.clientId }; + } + + for (const collection of getCollectionShortNames(config)) { const colConfig = config.collections[collection]; + const methods = getCollectionMethods(colConfig); - app.get(`/xrpc/${collection}.listRecords`, async (c) => { - const params = new URL(c.req.url).searchParams; - try { - const result = await runPipeline(db, config, collection, params); - return c.json(result); - } catch (e: any) { - if (e.message === "Could not resolve actor") { - return c.json({ error: e.message }, 400); + if (methods.includes("listRecords")) { + app.get(`/xrpc/${ns}.${collection}.listRecords`, async (c) => { + const params = new URL(c.req.url).searchParams; + const spaceUri = params.get("spaceUri") || undefined; + + if (spaceUri) { + const gated = await gateSpaceAccess(c, spaceUri, "read"); + if (gated instanceof Response) return gated; + // ACL passed — dispatch directly to the spaces adapter. + const nsid = colConfig.collection; + const list = await spacesCtx!.adapter.listRecords(spaceUri, nsid, { + byUser: params.get("byUser") ?? undefined, + cursor: params.get("cursor") ?? undefined, + limit: params.get("limit") ? Number(params.get("limit")) : undefined, + }); + return c.json(list); + } + + // Union path: when Authorization is present, verify the JWT and fold + // in records from spaces the caller is a member of. + let spaceUris: string[] | undefined; + if (spacesCtx && c.req.header("Authorization")) { + const nsid = new URL(c.req.url).pathname.match(/\/xrpc\/([^?]+)/)?.[1] as Nsid | null; + const auth = await verifyServiceAuthRequest(spacesCtx.verifier, c.req.raw, nsid); + if (!auth) { + return c.json( + { error: "AuthRequired", message: "invalid service-auth JWT" }, + 401 + ); + } + const { spaces } = await spacesCtx.adapter.listSpaces({ + memberDid: auth.issuer, + limit: 200, + }); + spaceUris = spaces.map((s) => s.uri); } - throw e; + + try { + const result = await runPipeline(db, config, collection, params, undefined, spaceUris); + return c.json(result); + } catch (e: any) { + if (e.message === "Could not resolve actor") { + return c.json({ error: e.message }, 400); + } + throw e; + } + }); + } + + if (!methods.includes("getRecord")) { + // Skip getRecord + custom queries unless listRecords-only was explicitly requested. + for (const [queryName, handler] of Object.entries(colConfig.queries ?? {})) { + app.get(`/xrpc/${ns}.${collection}.${queryName}`, async (c) => { + const params = new URL(c.req.url).searchParams; + return handler(db, params, config); + }); } - }); + continue; + } - app.get(`/xrpc/${collection}.getRecord`, async (c) => { + app.get(`/xrpc/${ns}.${collection}.getRecord`, async (c) => { const uri = c.req.query("uri"); if (!uri) return c.json({ error: "uri parameter required" }, 400); + // Spaces path — `?spaceUri=` routes to the per-space store + ACL gate. + const spaceUri = c.req.query("spaceUri") || undefined; + if (spaceUri) { + const gated = await gateSpaceAccess(c, spaceUri, "read"); + if (gated instanceof Response) return gated; + + // Parse author + rkey from the record uri `at:////` + const m = uri.match(/^at:\/\/([^/]+)\/[^/]+\/([^/]+)$/); + if (!m) return c.json({ error: "InvalidRequest", message: "uri must be at:////" }, 400); + const authorDid = m[1]; + const rkey = m[2]; + + const nsid = colConfig.collection; + const record = await spacesCtx!.adapter.getRecord(spaceUri, nsid, authorDid, rkey); + if (!record) return c.json({ error: "NotFound" }, 404); + return c.json({ record }); + } + const relations = colConfig.relations ?? {}; const references = colConfig.references ?? {}; const relMap = (config as ResolvedContrailConfig)._resolved?.relations[collection] ?? {}; @@ -207,7 +337,8 @@ export function registerCollectionRoutes( if (!row) return c.json({ error: "Record not found" }, 404); - const formatted = formatRecord({ ...row, collection }); + const nsid = nsidForShortName(config, collection) ?? collection; + const formatted = formatRecord({ ...row, collection: nsid }); const counts = extractCounts(row, relations); if (counts) flattenCounts(formatted, counts, relations); @@ -219,13 +350,15 @@ export function registerCollectionRoutes( db, relations, hydrateRequested.relations, - [row] + [row], + config ); const refs = await resolveReferences( db, references, hydrateRequested.references, - [row] + [row], + config ); const h = hydrates[row.uri]; if (h) { @@ -254,7 +387,7 @@ export function registerCollectionRoutes( for (const [queryName, handler] of Object.entries( colConfig.queries ?? {} )) { - app.get(`/xrpc/${collection}.${queryName}`, async (c) => { + app.get(`/xrpc/${ns}.${collection}.${queryName}`, async (c) => { const params = new URL(c.req.url).searchParams; return handler(db, params, config); }); @@ -263,7 +396,7 @@ export function registerCollectionRoutes( for (const [queryName, handler] of Object.entries( colConfig.pipelineQueries ?? {} )) { - app.get(`/xrpc/${collection}.${queryName}`, async (c) => { + app.get(`/xrpc/${ns}.${collection}.${queryName}`, async (c) => { const params = new URL(c.req.url).searchParams; try { const source = await handler(db, params, config); @@ -290,8 +423,8 @@ function getRelationCountColumns( cols.push({ column: countColumnName(rel.collection) }); const mapping = relMap[relName]; if (mapping?.groups) { - for (const [, fullToken] of Object.entries(mapping.groups as Record)) { - cols.push({ column: countColumnName(fullToken) }); + for (const groupKey of Object.keys(mapping.groups as Record)) { + cols.push({ column: groupedCountColumnName(rel.collection, groupKey) }); } } } @@ -311,8 +444,8 @@ function extractCounts( if (val != null && val !== 0) counts[rel.collection] = val; if (rel.groups) { - for (const [, fullToken] of Object.entries(rel.groups as Record)) { - const groupCol = countColumnName(fullToken); + for (const [groupKey, fullToken] of Object.entries(rel.groups as Record)) { + const groupCol = groupedCountColumnName(rel.collection, groupKey); const gval = row[groupCol]; if (gval != null && gval !== 0) counts[fullToken] = gval; } diff --git a/src/core/router/helpers.ts b/src/core/router/helpers.ts index 279e393..3aeb8d5 100644 --- a/src/core/router/helpers.ts +++ b/src/core/router/helpers.ts @@ -28,6 +28,7 @@ export function formatRecord(row: RecordRow): FormattedRecord { cid: row.cid, record, time_us: row.time_us, + ...(row._space ? { space: row._space } : {}), }; } diff --git a/src/core/router/hydrate.ts b/src/core/router/hydrate.ts index 1ce0e24..0d34207 100644 --- a/src/core/router/hydrate.ts +++ b/src/core/router/hydrate.ts @@ -1,8 +1,26 @@ -import type { RelationConfig, ReferenceConfig, RecordRow, Database } from "../types"; +import type { RelationConfig, ReferenceConfig, RecordRow, Database, ContrailConfig } from "../types"; import { getDialect } from "../dialect"; -import { getNestedValue, getRelationField, recordsTableName } from "../types"; +import { + getNestedValue, + getRelationField, + recordsTableName, + spacesRecordsTableName, + nsidForShortName, +} from "../types"; import { batchedInQuery, formatRecord } from "./helpers"; +/** Group rows by their origin: public (undefined key) or a specific spaceUri. */ +function groupBySource(rows: T[]): Map { + const groups = new Map(); + for (const r of rows) { + const key = r._space; + const g = groups.get(key); + if (g) g.push(r); + else groups.set(key, [r]); + } + return groups; +} + // --- Hydration: embed related records --- export function parseHydrateParams( @@ -41,59 +59,81 @@ export async function resolveHydrates( db: Database, relations: Record, requested: Record, - records: RecordRow[] + records: RecordRow[], + config?: ContrailConfig ): Promise { if (Object.keys(requested).length === 0 || records.length === 0) return {}; const grouped: Record>> = {}; + const sourceGroups = groupBySource(records); + for (const [relName, hydrateLimit] of Object.entries(requested)) { const rel = relations[relName]; const field = getRelationField(rel); const matchMode = rel.match ?? "uri"; - const table = recordsTableName(rel.collection); - - const matchValues = matchMode === "did" - ? [...new Set(records.map((r) => r.did))] - : records.map((r) => r.uri); - - if (matchValues.length === 0) continue; - - const groupCount = rel.groupBy ? 10 : 1; - const maxRows = matchValues.length * hydrateLimit * groupCount; - const relatedRows = await batchedInQuery>( - db, - `SELECT uri, did, rkey, record, time_us FROM ${table} - WHERE ${getDialect(db).jsonExtract('record', field)} IN (__IN__) - ORDER BY time_us DESC - LIMIT ${maxRows}`, - [], - matchValues - ); - - for (const row of relatedRows) { - const record = row.record ? JSON.parse(row.record) : null; - const matchedValue = getNestedValue(record, field); - if (!matchedValue) continue; - - const parentUris = matchMode === "did" - ? records.filter((r) => r.did === matchedValue).map((r) => r.uri) - : [matchedValue]; - - const groupValue = rel.groupBy - ? String(getNestedValue(record, rel.groupBy) ?? "other") - : "_flat"; - - for (const parentUri of parentUris) { - const targetUri = matchMode === "did" ? parentUri : matchedValue; - - if (!grouped[targetUri]) grouped[targetUri] = {}; - if (!grouped[targetUri][relName]) grouped[targetUri][relName] = {}; - if (!grouped[targetUri][relName][groupValue]) grouped[targetUri][relName][groupValue] = []; - - const group = grouped[targetUri][relName][groupValue]; - if (group.length < hydrateLimit) { - group.push(formatRecord({ ...row, collection: rel.collection })); + + for (const [sourceSpace, sourceRecords] of sourceGroups) { + const matchValues = matchMode === "did" + ? [...new Set(sourceRecords.map((r) => r.did))] + : sourceRecords.map((r) => r.uri); + + if (matchValues.length === 0) continue; + + const groupCount = rel.groupBy ? 10 : 1; + const maxRows = matchValues.length * hydrateLimit * groupCount; + + const table = sourceSpace + ? spacesRecordsTableName(rel.collection) + : recordsTableName(rel.collection); + const where = sourceSpace + ? `space_uri = ? AND ${getDialect(db).jsonExtract('record', field)} IN (__IN__)` + : `${getDialect(db).jsonExtract('record', field)} IN (__IN__)`; + const prefix = sourceSpace ? [sourceSpace] : []; + + const relatedRows = await batchedInQuery>( + db, + `SELECT uri, did, rkey, record, time_us FROM ${table} + WHERE ${where} + ORDER BY time_us DESC + LIMIT ${maxRows}`, + prefix, + matchValues + ); + + for (const row of relatedRows) { + const record = row.record ? JSON.parse(row.record) : null; + const matchedValue = getNestedValue(record, field); + if (!matchedValue) continue; + + const parentUris = matchMode === "did" + ? sourceRecords.filter((r) => r.did === matchedValue).map((r) => r.uri) + : [matchedValue]; + + const groupValue = rel.groupBy + ? String(getNestedValue(record, rel.groupBy) ?? "other") + : "_flat"; + + for (const parentUri of parentUris) { + const targetUri = matchMode === "did" ? parentUri : matchedValue; + + if (!grouped[targetUri]) grouped[targetUri] = {}; + if (!grouped[targetUri][relName]) grouped[targetUri][relName] = {}; + if (!grouped[targetUri][relName][groupValue]) grouped[targetUri][relName][groupValue] = []; + + const group = grouped[targetUri][relName][groupValue]; + if (group.length < hydrateLimit) { + const childNsid = config + ? nsidForShortName(config, rel.collection) ?? rel.collection + : rel.collection; + group.push( + formatRecord({ + ...(row as any), + collection: childNsid, + ...(sourceSpace ? { _space: sourceSpace } : {}), + } as RecordRow) + ); + } } } } @@ -122,43 +162,59 @@ export async function resolveReferences( db: Database, references: Record, requested: Set, - records: RecordRow[] + records: RecordRow[], + config?: ContrailConfig ): Promise { if (requested.size === 0 || records.length === 0) return {}; const result: ReferenceResult = {}; + const sourceGroups = groupBySource(records); + for (const refName of requested) { const ref = references[refName]; if (!ref) continue; - const table = recordsTableName(ref.collection); - - const targetMap = new Map(); - for (const r of records) { - const parsed = r.record ? JSON.parse(r.record) : null; - const targetValue = parsed ? getNestedValue(parsed, ref.field) : null; - if (!targetValue) continue; - if (!targetMap.has(targetValue)) targetMap.set(targetValue, []); - targetMap.get(targetValue)!.push(r.uri); - } + const refNsid = config + ? nsidForShortName(config, ref.collection) ?? ref.collection + : ref.collection; + + for (const [sourceSpace, sourceRecords] of sourceGroups) { + const targetMap = new Map(); + for (const r of sourceRecords) { + const parsed = r.record ? JSON.parse(r.record) : null; + const targetValue = parsed ? getNestedValue(parsed, ref.field) : null; + if (!targetValue) continue; + if (!targetMap.has(targetValue)) targetMap.set(targetValue, []); + targetMap.get(targetValue)!.push(r.uri); + } - const targetUris = [...targetMap.keys()]; - if (targetUris.length === 0) continue; - - const rows = await batchedInQuery>( - db, - `SELECT uri, did, rkey, record, time_us FROM ${table} - WHERE uri IN (__IN__)`, - [], - targetUris - ); - - for (const row of rows) { - const parentUris = targetMap.get(row.uri) ?? []; - for (const parentUri of parentUris) { - if (!result[parentUri]) result[parentUri] = {}; - result[parentUri][refName] = formatRecord({ ...row, collection: ref.collection }); + const targetUris = [...targetMap.keys()]; + if (targetUris.length === 0) continue; + + const table = sourceSpace + ? spacesRecordsTableName(ref.collection) + : recordsTableName(ref.collection); + const where = sourceSpace ? `space_uri = ? AND uri IN (__IN__)` : `uri IN (__IN__)`; + const prefix = sourceSpace ? [sourceSpace] : []; + + const rows = await batchedInQuery>( + db, + `SELECT uri, did, rkey, record, time_us FROM ${table} WHERE ${where}`, + prefix, + targetUris + ); + + for (const row of rows) { + const parentUris = targetMap.get(row.uri) ?? []; + for (const parentUri of parentUris) { + if (!result[parentUri]) result[parentUri] = {}; + result[parentUri][refName] = formatRecord({ + ...(row as any), + collection: refNsid, + ...(sourceSpace ? { _space: sourceSpace } : {}), + } as RecordRow); + } } } } diff --git a/src/core/router/index.ts b/src/core/router/index.ts index 8b21045..3eba9c9 100644 --- a/src/core/router/index.ts +++ b/src/core/router/index.ts @@ -8,14 +8,25 @@ import { registerFeedRoutes } from "./feed"; import { registerNotifyRoute } from "./notify"; import { registerSpacesRoutes } from "../spaces/router"; import type { SpacesRoutesOptions } from "../spaces/router"; +import { buildVerifier } from "../spaces/auth"; +import { HostedAdapter } from "../spaces/adapter"; +import type { StorageAdapter } from "../spaces/types"; +import type { ServiceJwtVerifier } from "@atcute/xrpc-server/auth"; import { resolveActor } from "../identity"; import { resolveProfiles } from "./profiles"; import { backfillUser } from "../backfill"; +export interface SpacesContext { + adapter: StorageAdapter; + verifier: ServiceJwtVerifier; +} + export interface CreateAppOptions { spaces?: SpacesRoutesOptions; /** Separate DB for the spaces tables. Defaults to `db`. */ spacesDb?: Database; + /** Full spaces context override (escape hatch for tests). */ + spacesCtx?: SpacesContext | null; } export function createApp( @@ -52,11 +63,24 @@ export function createApp( return c.json({ profiles }); }); + // Shared spaces context — verifier + adapter — reused by both the per-collection + // routes (for `?spaceUri=...` dispatch) and the `.space.*` routes. + const spacesDb = options.spacesDb ?? db; + const spacesCtx: SpacesContext | null = + options.spacesCtx !== undefined + ? options.spacesCtx + : config.spaces + ? { + adapter: options.spaces?.adapter ?? new HostedAdapter(spacesDb, config), + verifier: buildVerifier(config.spaces), + } + : null; + registerAdminRoutes(app, db, config); - registerCollectionRoutes(app, db, config); + registerCollectionRoutes(app, db, config, spacesCtx); registerFeedRoutes(app, db, config); registerNotifyRoute(app, db, config); - registerSpacesRoutes(app, options.spacesDb ?? db, config, options.spaces); + registerSpacesRoutes(app, spacesDb, config, options.spaces, spacesCtx); return app; } diff --git a/src/core/router/notify.ts b/src/core/router/notify.ts index e9fddad..5ff630b 100644 --- a/src/core/router/notify.ts +++ b/src/core/router/notify.ts @@ -1,5 +1,6 @@ import type { Hono } from "hono"; import type { Database, ContrailConfig, IngestEvent } from "../types"; +import { shortNameForNsid } from "../types"; import { applyEvents, lookupExistingRecords } from "../db/records"; import { getPDS } from "../client"; import type { Did } from "@atcute/lexicons"; @@ -60,7 +61,8 @@ export async function processNotifyUris( errors.push(`invalid AT URI: ${uri}`); continue; } - if (!config.collections[parsed.collection]) { + // `parsed.collection` is an NSID; look up the matching short name. + if (!shortNameForNsid(config, parsed.collection)) { errors.push(`collection not tracked: ${parsed.collection}`); continue; } @@ -71,7 +73,8 @@ export async function processNotifyUris( const existing = await lookupExistingRecords( db, validUris.map(({ uri, parsed }) => ({ uri, collection: parsed.collection })), - true + true, + config ); for (const { uri, parsed } of validUris) { diff --git a/src/core/router/profiles.ts b/src/core/router/profiles.ts index 491c9d2..fb0cc39 100644 --- a/src/core/router/profiles.ts +++ b/src/core/router/profiles.ts @@ -47,9 +47,9 @@ export async function resolveProfiles( // Batch-lookup profile records for each configured profile collection for (const pc of profileConfigs) { - const { collection, rkey: configRkey } = pc; + const { collection, rkey: configRkey, shortName } = pc; const rkey = configRkey ?? "self"; - const table = recordsTableName(collection); + const table = recordsTableName(shortName ?? collection); const uris = dids.map((did) => `at://${did}/${collection}/${rkey}`); const rows = await batchedInQuery>( @@ -127,9 +127,9 @@ async function fetchMissingProfiles( await Promise.all( dids.flatMap((did) => profileConfigs.map(async (pc) => { - const { collection, rkey: configRkey } = pc; + const { collection, rkey: configRkey, shortName } = pc; const rkey = configRkey ?? "self"; - const table = recordsTableName(collection); + const table = recordsTableName(shortName ?? collection); try { const pds = await getPDS(did as Did, db); if (!pds) return; diff --git a/src/core/spaces/acl.ts b/src/core/spaces/acl.ts index 21b56a6..05adc40 100644 --- a/src/core/spaces/acl.ts +++ b/src/core/spaces/acl.ts @@ -1,16 +1,9 @@ -import type { - AppPolicy, - CollectionPolicy, - SpaceMemberRow, - SpaceRow, - SpacesConfig, -} from "./types"; +import type { AppPolicy, MemberPerm, SpaceMemberRow, SpaceRow } from "./types"; export type AclOp = "read" | "write" | "delete"; export interface AclInput { op: AclOp; - collection: string; space: SpaceRow; callerDid: string; /** Membership row for the caller (or null). Owner does not require a row. */ @@ -19,36 +12,19 @@ export interface AclInput { clientId?: string; /** For per-record ops (get/delete), the record's author DID. */ targetAuthorDid?: string; - /** The service's configured defaults, used when the space has no override. */ - config: Pick; } export type AclResult = - | { allow: true; policy: CollectionPolicy } - | { allow: false; reason: AclDenyReason; policy?: CollectionPolicy }; + | { allow: true } + | { allow: false; reason: AclDenyReason }; export type AclDenyReason = - | "no-policy" | "not-member" - | "not-owner" + | "not-writer" | "not-own-record" | "app-not-allowed" | "unknown-op"; -/** Resolve the effective policy for a given collection in a given space. */ -export function resolveCollectionPolicy( - space: SpaceRow, - collection: string, - config: Pick -): CollectionPolicy | null { - return ( - space.policy?.[collection] ?? - config.defaultPolicies?.[collection] ?? - config.defaultPolicy ?? - null - ); -} - /** Check whether the caller's app is permitted to act in this space. */ export function checkAppPolicy( appPolicy: AppPolicy | null, @@ -61,61 +37,44 @@ export function checkAppPolicy( } const isOwner = (space: SpaceRow, did: string) => space.ownerDid === did; -const isMember = (space: SpaceRow, member: SpaceMemberRow | null, did: string) => +const hasMember = (space: SpaceRow, member: SpaceMemberRow | null, did: string) => isOwner(space, did) || member != null; +const hasWrite = (space: SpaceRow, member: SpaceMemberRow | null, did: string) => + isOwner(space, did) || member?.perms === "write"; +/** Space-level access check. + * Model matches the proposal: member list is a (DID, perm) tuple set per space; + * write implies read; owner is always implicit write. No per-collection + * policies — all records in a space share the same access rule. */ export function checkAccess(input: AclInput): AclResult { - const policy = resolveCollectionPolicy(input.space, input.collection, input.config); - if (!policy) return { allow: false, reason: "no-policy" }; - if (!checkAppPolicy(input.space.appPolicy, input.clientId)) { - return { allow: false, reason: "app-not-allowed", policy }; + return { allow: false, reason: "app-not-allowed" }; } if (input.op === "read") { - switch (policy.read) { - case "owner": - return isOwner(input.space, input.callerDid) - ? { allow: true, policy } - : { allow: false, reason: "not-owner", policy }; - case "member": - return isMember(input.space, input.member, input.callerDid) - ? { allow: true, policy } - : { allow: false, reason: "not-member", policy }; - case "member-own": - if (!isMember(input.space, input.member, input.callerDid)) { - return { allow: false, reason: "not-member", policy }; - } - if (input.targetAuthorDid && input.targetAuthorDid !== input.callerDid) { - return { allow: false, reason: "not-own-record", policy }; - } - return { allow: true, policy }; - } + return hasMember(input.space, input.member, input.callerDid) + ? { allow: true } + : { allow: false, reason: "not-member" }; } if (input.op === "write") { - switch (policy.write) { - case "owner": - return isOwner(input.space, input.callerDid) - ? { allow: true, policy } - : { allow: false, reason: "not-owner", policy }; - case "member": - return isMember(input.space, input.member, input.callerDid) - ? { allow: true, policy } - : { allow: false, reason: "not-member", policy }; - } + return hasWrite(input.space, input.member, input.callerDid) + ? { allow: true } + : { allow: false, reason: "not-writer" }; } if (input.op === "delete") { - if (isOwner(input.space, input.callerDid)) return { allow: true, policy }; - if (!isMember(input.space, input.member, input.callerDid)) { - return { allow: false, reason: "not-member", policy }; + if (isOwner(input.space, input.callerDid)) return { allow: true }; + if (!hasWrite(input.space, input.member, input.callerDid)) { + return { allow: false, reason: "not-writer" }; } if (input.targetAuthorDid && input.targetAuthorDid !== input.callerDid) { - return { allow: false, reason: "not-own-record", policy }; + return { allow: false, reason: "not-own-record" }; } - return { allow: true, policy }; + return { allow: true }; } return { allow: false, reason: "unknown-op" }; } + +export type MemberPermExport = MemberPerm; diff --git a/src/core/spaces/adapter.ts b/src/core/spaces/adapter.ts index 3d5f3ee..79d719e 100644 --- a/src/core/spaces/adapter.ts +++ b/src/core/spaces/adapter.ts @@ -1,13 +1,22 @@ -import type { Database } from "../types"; +import type { ContrailConfig, Database, RelationConfig, ResolvedContrailConfig } from "../types"; +import { + shortNameForNsid, + spacesRecordsTableName, + countColumnName, + groupedCountColumnName, + getRelationField, + getNestedValue, +} from "../types"; +import { getDialect } from "../dialect"; import type { AppPolicy, CollectionCount, - CollectionPolicy, CreateInviteInput, InviteRow, ListOptions, ListResult, ListSpacesOptions, + MemberPerm, SpaceMemberRow, SpaceRow, StorageAdapter, @@ -39,7 +48,6 @@ function mapSpaceRow(row: any): SpaceRow { serviceDid: row.service_did, memberListRef: row.member_list_ref ?? null, appPolicyRef: row.app_policy_ref ?? null, - policy: parseJson>(row.policy), appPolicy: parseJson(row.app_policy), createdAt: toNum(row.created_at), deletedAt: row.deleted_at == null ? null : toNum(row.deleted_at), @@ -50,7 +58,7 @@ function mapMemberRow(row: any): SpaceMemberRow { return { spaceUri: row.space_uri, did: row.did, - perms: row.perms, + perms: row.perms as MemberPerm, addedAt: toNum(row.added_at), addedBy: row.added_by ?? null, }; @@ -71,27 +79,49 @@ function mapInviteRow(row: any): InviteRow { }; } -function mapRecordRow(row: any): StoredRecord { +/** Row mapper for per-collection spaces_records_ tables. + * `collection` is injected by the caller (known from the table name). */ +function mapRecordRow(row: any, collection: string): StoredRecord { return { spaceUri: row.space_uri, - collection: row.collection, - authorDid: row.author_did, + collection, + authorDid: row.did, rkey: row.rkey, cid: row.cid ?? null, record: parseJson>(row.record) ?? {}, - createdAt: toNum(row.created_at), + createdAt: toNum(row.time_us), }; } export class HostedAdapter implements StorageAdapter { - constructor(private readonly db: Database) {} + constructor( + private readonly db: Database, + private readonly config?: ContrailConfig + ) {} + + /** Resolve the per-collection spaces table name, or throw if the collection + * isn't configured (and therefore has no table). */ + private tableFor(collection: string): string { + if (!this.config) { + throw new Error( + `HostedAdapter: config not provided; cannot resolve table for collection ${collection}` + ); + } + const short = shortNameForNsid(this.config, collection); + if (!short) { + throw new Error( + `HostedAdapter: collection ${collection} is not configured in this deployment` + ); + } + return spacesRecordsTableName(short); + } async createSpace(space: Omit): Promise { const now = Date.now(); await this.db .prepare( - `INSERT INTO spaces (uri, owner_did, type, key, service_did, member_list_ref, app_policy_ref, policy, app_policy, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + `INSERT INTO spaces (uri, owner_did, type, key, service_did, member_list_ref, app_policy_ref, app_policy, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` ) .bind( space.uri, @@ -101,7 +131,6 @@ export class HostedAdapter implements StorageAdapter { space.serviceDid, space.memberListRef, space.appPolicyRef, - space.policy ? JSON.stringify(space.policy) : null, space.appPolicy ? JSON.stringify(space.appPolicy) : null, now ) @@ -164,13 +193,6 @@ export class HostedAdapter implements StorageAdapter { .run(); } - async updateSpacePolicy(spaceUri: string, policy: Record): Promise { - await this.db - .prepare(`UPDATE spaces SET policy = ? WHERE uri = ?`) - .bind(JSON.stringify(policy), spaceUri) - .run(); - } - async updateSpaceAppPolicy(spaceUri: string, appPolicy: AppPolicy): Promise { await this.db .prepare(`UPDATE spaces SET app_policy = ? WHERE uri = ?`) @@ -178,7 +200,7 @@ export class HostedAdapter implements StorageAdapter { .run(); } - async addMember(spaceUri: string, did: string, perms: string, addedBy: string | null): Promise { + async addMember(spaceUri: string, did: string, perms: MemberPerm, addedBy: string | null): Promise { await this.db .prepare( `INSERT INTO spaces_members (space_uri, did, perms, added_at, added_by) @@ -288,23 +310,50 @@ export class HostedAdapter implements StorageAdapter { } async putRecord(record: StoredRecord): Promise { + const table = this.tableFor(record.collection); + const uri = `at://${record.authorDid}/${record.collection}/${record.rkey}`; + + const childShort = this.config ? shortNameForNsid(this.config, record.collection) : null; + const prev = childShort + ? await this.db + .prepare(`SELECT record FROM ${table} WHERE space_uri = ? AND did = ? AND rkey = ?`) + .bind(record.spaceUri, record.authorDid, record.rkey) + .first<{ record: unknown } | null>() + : null; + const beforeRecord = parseJson>(prev?.record ?? null); + await this.db .prepare( - `INSERT INTO spaces_records (space_uri, collection, author_did, rkey, cid, record, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?) - ON CONFLICT (space_uri, collection, author_did, rkey) DO UPDATE SET - cid = excluded.cid, record = excluded.record` + `INSERT INTO ${table} (space_uri, uri, did, rkey, cid, record, time_us, indexed_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (space_uri, did, rkey) DO UPDATE SET + uri = excluded.uri, + cid = excluded.cid, + record = excluded.record, + time_us = excluded.time_us, + indexed_at = excluded.indexed_at` ) .bind( record.spaceUri, - record.collection, + uri, record.authorDid, record.rkey, record.cid, JSON.stringify(record.record), - record.createdAt + record.createdAt, + Date.now() ) .run(); + + if (childShort && this.config) { + await this.recountParentsForSpace( + record.spaceUri, + childShort, + beforeRecord, + record.record, + record.authorDid + ); + } } async getRecord( @@ -313,14 +362,15 @@ export class HostedAdapter implements StorageAdapter { authorDid: string, rkey: string ): Promise { + const table = this.tableFor(collection); const row = await this.db .prepare( - `SELECT * FROM spaces_records - WHERE space_uri = ? AND collection = ? AND author_did = ? AND rkey = ?` + `SELECT * FROM ${table} + WHERE space_uri = ? AND did = ? AND rkey = ?` ) - .bind(spaceUri, collection, authorDid, rkey) + .bind(spaceUri, authorDid, rkey) .first(); - return row ? mapRecordRow(row) : null; + return row ? mapRecordRow(row, collection) : null; } async listRecords( @@ -328,27 +378,28 @@ export class HostedAdapter implements StorageAdapter { collection: string, options: ListOptions = {} ): Promise { + const table = this.tableFor(collection); const limit = Math.min(options.limit ?? 50, 200); - const clauses: string[] = ["space_uri = ?", "collection = ?"]; - const params: any[] = [spaceUri, collection]; + const clauses: string[] = ["space_uri = ?"]; + const params: any[] = [spaceUri]; if (options.byUser) { - clauses.push("author_did = ?"); + clauses.push("did = ?"); params.push(options.byUser); } if (options.cursor) { - clauses.push("created_at < ?"); + clauses.push("time_us < ?"); params.push(Number(options.cursor)); } - const sql = `SELECT * FROM spaces_records + const sql = `SELECT * FROM ${table} WHERE ${clauses.join(" AND ")} - ORDER BY created_at DESC + ORDER BY time_us DESC LIMIT ?`; params.push(limit + 1); const { results } = await this.db.prepare(sql).bind(...params).all(); - const records = results.map(mapRecordRow); + const records = results.map((r) => mapRecordRow(r, collection)); let cursor: string | undefined; if (records.length > limit) { const next = records.pop()!; @@ -363,30 +414,152 @@ export class HostedAdapter implements StorageAdapter { authorDid: string, rkey: string ): Promise { + const table = this.tableFor(collection); + + const childShort = this.config ? shortNameForNsid(this.config, collection) : null; + const prev = childShort + ? await this.db + .prepare(`SELECT record FROM ${table} WHERE space_uri = ? AND did = ? AND rkey = ?`) + .bind(spaceUri, authorDid, rkey) + .first<{ record: unknown } | null>() + : null; + const beforeRecord = parseJson>(prev?.record ?? null); + await this.db .prepare( - `DELETE FROM spaces_records - WHERE space_uri = ? AND collection = ? AND author_did = ? AND rkey = ?` + `DELETE FROM ${table} + WHERE space_uri = ? AND did = ? AND rkey = ?` ) - .bind(spaceUri, collection, authorDid, rkey) + .bind(spaceUri, authorDid, rkey) .run(); + + if (childShort && this.config) { + await this.recountParentsForSpace(spaceUri, childShort, beforeRecord, null, authorDid); + } + } + + /** Recompute count columns on parent records in the same space, scoped to the + * targets derived from before/after versions of the written/deleted child record. */ + private async recountParentsForSpace( + spaceUri: string, + childShort: string, + before: Record | null, + after: Record | null, + childDid: string + ): Promise { + if (!this.config) return; + const config = this.config; + const resolved = (config as ResolvedContrailConfig)._resolved; + const childTable = spacesRecordsTableName(childShort); + + type Inbound = { parentShort: string; relationName: string; rel: RelationConfig }; + const inbound: Inbound[] = []; + for (const [parentShort, parentCfg] of Object.entries(config.collections)) { + if (parentCfg.allowInSpaces === false) continue; + for (const [relName, rel] of Object.entries(parentCfg.relations ?? {})) { + if (rel.count === false) continue; + if (rel.collection !== childShort) continue; + inbound.push({ parentShort, relationName: relName, rel }); + } + } + if (inbound.length === 0) return; + + // Deduplicate (parent, relation, target) across before/after. + const keyed = new Map(); + for (const { parentShort, relationName, rel } of inbound) { + const field = getRelationField(rel); + const collectTarget = (rec: Record | null) => { + if (!rec) return; + if (rel.match === "did") { + keyed.set(`${parentShort}:${relationName}:${childDid}`, { + parentShort, relationName, rel, target: childDid, + }); + return; + } + const v = getNestedValue(rec, field); + if (typeof v === "string" && v.length > 0) { + keyed.set(`${parentShort}:${relationName}:${v}`, { + parentShort, relationName, rel, target: v, + }); + } + }; + collectTarget(before); + collectTarget(after); + } + if (keyed.size === 0) return; + + const dialect = getDialect(this.db); + const stmts: ReturnType[] = []; + + for (const { parentShort, relationName, rel, target } of keyed.values()) { + const parentTable = spacesRecordsTableName(parentShort); + const matchColumn = rel.match === "did" ? "did" : "uri"; + const field = getRelationField(rel); + const countExpr = rel.countDistinct + ? `COUNT(DISTINCT ${rel.countDistinct})` + : "COUNT(*)"; + + const setClauses: string[] = []; + const binds: (string | number)[] = []; + + const totalCol = countColumnName(rel.collection); + setClauses.push( + `${totalCol} = (SELECT ${countExpr} FROM ${childTable} WHERE space_uri = ? AND ${dialect.jsonExtract("record", field)} = ?)` + ); + binds.push(spaceUri, target); + + if (rel.groupBy) { + const mapping = resolved?.relations[parentShort]?.[relationName]; + if (mapping?.groups) { + for (const [groupKey, fullToken] of Object.entries(mapping.groups)) { + const groupCol = groupedCountColumnName(rel.collection, groupKey); + setClauses.push( + `${groupCol} = (SELECT ${countExpr} FROM ${childTable} WHERE space_uri = ? AND ${dialect.jsonExtract("record", field)} = ? AND ${dialect.jsonExtract("record", rel.groupBy)} = ?)` + ); + binds.push(spaceUri, target, fullToken); + } + } + } + + binds.push(spaceUri, target); + stmts.push( + this.db + .prepare( + `UPDATE ${parentTable} SET ${setClauses.join(", ")} WHERE space_uri = ? AND ${matchColumn} = ?` + ) + .bind(...binds) + ); + } + + if (stmts.length > 0) await this.db.batch(stmts); } async listCollections( spaceUri: string, options: { byUser?: string } = {} ): Promise { - const clauses: string[] = ["space_uri = ?"]; - const params: any[] = [spaceUri]; - if (options.byUser) { - clauses.push("author_did = ?"); - params.push(options.byUser); + if (!this.config) return []; + const results: CollectionCount[] = []; + for (const [short, colConfig] of Object.entries(this.config.collections)) { + if (colConfig.allowInSpaces === false) continue; + const table = spacesRecordsTableName(short); + const clauses: string[] = ["space_uri = ?"]; + const params: any[] = [spaceUri]; + if (options.byUser) { + clauses.push("did = ?"); + params.push(options.byUser); + } + try { + const row = await this.db + .prepare(`SELECT COUNT(*) AS count FROM ${table} WHERE ${clauses.join(" AND ")}`) + .bind(...params) + .first<{ count: number }>(); + const count = Number(row?.count ?? 0); + if (count > 0) results.push({ collection: colConfig.collection, count }); + } catch { + // table doesn't exist (collection added after init, or allowInSpaces toggled) — skip + } } - const sql = `SELECT collection, COUNT(*) AS count - FROM spaces_records - WHERE ${clauses.join(" AND ")} - GROUP BY collection`; - const { results } = await this.db.prepare(sql).bind(...params).all(); - return results.map((r) => ({ collection: r.collection, count: Number(r.count) })); + return results; } } diff --git a/src/core/spaces/auth.ts b/src/core/spaces/auth.ts index 17b5d16..548583a 100644 --- a/src/core/spaces/auth.ts +++ b/src/core/spaces/auth.ts @@ -1,7 +1,32 @@ import type { Context, MiddlewareHandler } from "hono"; import { ServiceJwtVerifier } from "@atcute/xrpc-server/auth"; -import type { DidDocumentResolver } from "@atcute/identity-resolver"; +import { + CompositeDidDocumentResolver, + PlcDidDocumentResolver, + WebDidDocumentResolver, + type DidDocumentResolver, +} from "@atcute/identity-resolver"; import type { Did, Nsid } from "@atcute/lexicons"; +import type { SpacesConfig } from "./types"; + +export { ServiceJwtVerifier }; + +/** Build a ServiceJwtVerifier from a SpacesConfig, using the configured + * resolver or a default PLC+Web composite. */ +export function buildVerifier(spaces: SpacesConfig): ServiceJwtVerifier { + const resolver = + spaces.resolver ?? + new CompositeDidDocumentResolver({ + methods: { + plc: new PlcDidDocumentResolver(), + web: new WebDidDocumentResolver(), + }, + }); + return new ServiceJwtVerifier({ + serviceDid: spaces.serviceDid as Did, + resolver, + }); +} export interface ServiceAuth { issuer: string; @@ -20,13 +45,8 @@ export interface ServiceAuthOptions { * service-auth token. On success, attaches the decoded claims to c.var.serviceAuth. * Expected Nsid method is taken from the route pattern (last segment after /xrpc/). */ export function createServiceAuthMiddleware( - options: ServiceAuthOptions + verifier: ServiceJwtVerifier ): MiddlewareHandler { - const verifier = new ServiceJwtVerifier({ - serviceDid: options.serviceDid, - resolver: options.resolver, - }); - return async (c, next) => { const header = c.req.header("Authorization"); if (!header || !header.startsWith("Bearer ")) { @@ -62,3 +82,23 @@ export function requireServiceAuth(c: Context): ServiceAuth { if (!auth) throw new Error("service auth missing; middleware not attached"); return auth; } + +/** Verify a request's Authorization: Bearer token out-of-band (e.g. from a + * route handler that doesn't always require auth). Returns the claims on + * success, or null if missing/invalid. */ +export async function verifyServiceAuthRequest( + verifier: ServiceJwtVerifier, + request: Request, + lxm?: Nsid | null +): Promise { + const header = request.headers.get("Authorization"); + if (!header || !header.startsWith("Bearer ")) return null; + const token = header.slice(7).trim(); + const result = await verifier.verify(token, { lxm: lxm ?? null }); + if (!result.ok) return null; + return { + issuer: result.value.issuer, + audience: result.value.audience, + lxm: result.value.lxm, + }; +} diff --git a/src/core/spaces/router.ts b/src/core/spaces/router.ts index fd50249..2477e3c 100644 --- a/src/core/spaces/router.ts +++ b/src/core/spaces/router.ts @@ -1,16 +1,14 @@ import type { Hono, MiddlewareHandler } from "hono"; import type { ContrailConfig, Database } from "../types"; import { HostedAdapter } from "./adapter"; -import { checkAccess, resolveCollectionPolicy } from "./acl"; +import { checkAccess } from "./acl"; import type { ServiceAuth } from "./auth"; -import { createServiceAuthMiddleware } from "./auth"; +import { buildVerifier, createServiceAuthMiddleware } from "./auth"; import { nextTid } from "./tid"; import { generateInviteToken, hashInviteToken } from "./invite-token"; -import type { CollectionPolicy, InviteRow, SpaceRow, SpacesConfig, StorageAdapter } from "./types"; +import type { InviteRow, MemberPerm, SpaceRow, SpacesConfig, StorageAdapter } from "./types"; import type { Did } from "@atcute/lexicons"; -const SPACE = "tools.atmo.space"; - export interface SpacesRoutesOptions { /** Provide a custom middleware (e.g. for tests). If omitted and spaces.resolver is set, a real one is built. */ authMiddleware?: MiddlewareHandler; @@ -22,16 +20,58 @@ export function registerSpacesRoutes( app: Hono, db: Database, config: ContrailConfig, - options: SpacesRoutesOptions = {} + options: SpacesRoutesOptions = {}, + ctx?: { adapter: StorageAdapter; verifier: import("@atcute/xrpc-server/auth").ServiceJwtVerifier } | null ): void { const spacesConfig = config.spaces; if (!spacesConfig) return; - const adapter = options.adapter ?? new HostedAdapter(db); - const auth = options.authMiddleware ?? buildAuthMiddleware(spacesConfig); - if (!auth) return; // no resolver configured — spaces are effectively disabled + const adapter = options.adapter ?? ctx?.adapter ?? new HostedAdapter(db, config); + const auth = + options.authMiddleware ?? + (ctx ? createServiceAuthMiddleware(ctx.verifier) : buildAuthMiddleware(spacesConfig)); + + /** Space endpoints are emitted per-deployment under the configured namespace; + * the deployment owns and publishes its own lexicons. The library ships + * templates at `lexicons/tools/atmo/space/*` that the generator instantiates + * under `.space.*`. */ + const SPACE = `${config.namespace}.space`; // Read endpoints + app.get(`/xrpc/${SPACE}.listSpaces`, auth, async (c) => { + const sa = getAuth(c); + const scope = c.req.query("scope") ?? "member"; // "member" | "owner" + const type = c.req.query("type") ?? undefined; + const cursor = c.req.query("cursor") ?? undefined; + const limit = c.req.query("limit") ? Number(c.req.query("limit")) : undefined; + + const opts: Parameters[0] = { type, cursor, limit }; + if (scope === "owner") opts.ownerDid = sa.issuer; + else opts.memberDid = sa.issuer; + + const result = await adapter.listSpaces(opts); + return c.json({ + spaces: result.spaces.map((s) => publicSpaceView(s, s.ownerDid === sa.issuer)), + cursor: result.cursor, + }); + }); + + app.get(`/xrpc/${SPACE}.listMembers`, auth, async (c) => { + const sa = getAuth(c); + const spaceUri = c.req.query("spaceUri"); + if (!spaceUri) return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); + const space = await adapter.getSpace(spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + + const isOwner = space.ownerDid === sa.issuer; + const member = isOwner ? null : await adapter.getMember(spaceUri, sa.issuer); + if (!isOwner && !member) { + return c.json({ error: "Forbidden", reason: "not-member" }, 403); + } + const members = await adapter.listMembers(spaceUri); + return c.json({ members }); + }); + app.get(`/xrpc/${SPACE}.getSpace`, auth, async (c) => { const uri = c.req.query("uri"); if (!uri) return c.json({ error: "InvalidRequest", message: "uri required" }, 400); @@ -60,24 +100,17 @@ export function registerSpacesRoutes( const member = await adapter.getMember(spaceUri, sa.issuer); const result = checkAccess({ op: "read", - collection, space, callerDid: sa.issuer, member, clientId: sa.clientId, - config: spacesConfig, }); if (!result.allow) { return c.json({ error: "Forbidden", reason: result.reason }, 403); } - // member-own: force caller-only filter - const byUserParam = c.req.query("byUser") ?? undefined; - const byUser = - result.policy.read === "member-own" ? sa.issuer : byUserParam; - const list = await adapter.listRecords(spaceUri, collection, { - byUser, + byUser: c.req.query("byUser") ?? undefined, cursor: c.req.query("cursor") ?? undefined, limit: c.req.query("limit") ? Number(c.req.query("limit")) : undefined, }); @@ -99,13 +132,11 @@ export function registerSpacesRoutes( const member = await adapter.getMember(spaceUri, sa.issuer); const result = checkAccess({ op: "read", - collection, space, callerDid: sa.issuer, member, clientId: sa.clientId, targetAuthorDid: author, - config: spacesConfig, }); if (!result.allow) return c.json({ error: "Forbidden", reason: result.reason }, 403); @@ -129,12 +160,10 @@ export function registerSpacesRoutes( const member = await adapter.getMember(body.spaceUri, sa.issuer); const result = checkAccess({ op: "write", - collection: body.collection, space, callerDid: sa.issuer, member, clientId: sa.clientId, - config: spacesConfig, }); if (!result.allow) return c.json({ error: "Forbidden", reason: result.reason }, 403); @@ -152,13 +181,38 @@ export function registerSpacesRoutes( return c.json({ rkey, authorDid: sa.issuer, createdAt: now }); }); + app.post(`/xrpc/${SPACE}.deleteRecord`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { spaceUri?: string; collection?: string; rkey?: string } + | null; + if (!body?.spaceUri || !body.collection || !body.rkey) { + return c.json({ error: "InvalidRequest", message: "spaceUri, collection, rkey required" }, 400); + } + const space = await adapter.getSpace(body.spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + + const member = await adapter.getMember(body.spaceUri, sa.issuer); + const result = checkAccess({ + op: "delete", + space, + callerDid: sa.issuer, + member, + clientId: sa.clientId, + targetAuthorDid: sa.issuer, + }); + if (!result.allow) return c.json({ error: "Forbidden", reason: result.reason }, 403); + + await adapter.deleteRecord(body.spaceUri, body.collection, sa.issuer, body.rkey); + return c.json({ ok: true }); + }); + // Admin endpoints app.post(`/xrpc/${SPACE}.admin.createSpace`, auth, async (c) => { const sa = getAuth(c); const body = (await c.req.json().catch(() => ({}))) as { type?: string; key?: string; - policy?: Record; appPolicy?: SpaceRow["appPolicy"]; memberListRef?: string; appPolicyRef?: string; @@ -179,10 +233,10 @@ export function registerSpacesRoutes( serviceDid: spacesConfig.serviceDid, memberListRef: body.memberListRef ?? null, appPolicyRef: body.appPolicyRef ?? null, - policy: body.policy ?? null, appPolicy: body.appPolicy ?? spacesConfig.defaultAppPolicy ?? null, }); - await adapter.addMember(uri, sa.issuer, "owner", sa.issuer); + // Owner is implicit; we still write a row so membership queries are uniform. + await adapter.addMember(uri, sa.issuer, "write", sa.issuer); return c.json({ space: publicSpaceView(space, true) }); }); @@ -191,7 +245,7 @@ export function registerSpacesRoutes( app.post(`/xrpc/${SPACE}.invite.create`, auth, async (c) => { const sa = getAuth(c); const body = (await c.req.json().catch(() => null)) as - | { spaceUri?: string; perms?: string; expiresAt?: number; maxUses?: number; note?: string } + | { spaceUri?: string; perms?: MemberPerm; expiresAt?: number; maxUses?: number; note?: string } | null; if (!body?.spaceUri) { return c.json({ error: "InvalidRequest", message: "spaceUri required" }, 400); @@ -207,7 +261,7 @@ export function registerSpacesRoutes( const invite = await adapter.createInvite({ spaceUri: body.spaceUri, tokenHash, - perms: body.perms ?? "member", + perms: body.perms ?? "write", expiresAt: body.expiresAt ?? null, maxUses: body.maxUses ?? null, createdBy: sa.issuer, @@ -265,7 +319,7 @@ export function registerSpacesRoutes( app.post(`/xrpc/${SPACE}.admin.addMember`, auth, async (c) => { const sa = getAuth(c); const body = (await c.req.json().catch(() => null)) as - | { spaceUri?: string; did?: string; perms?: string } + | { spaceUri?: string; did?: string; perms?: MemberPerm } | null; if (!body?.spaceUri || !body.did) { return c.json({ error: "InvalidRequest", message: "spaceUri and did required" }, 400); @@ -275,19 +329,36 @@ export function registerSpacesRoutes( if (space.ownerDid !== sa.issuer) { return c.json({ error: "Forbidden", reason: "not-owner" }, 403); } - await adapter.addMember(body.spaceUri, body.did, body.perms ?? "member", sa.issuer); + await adapter.addMember(body.spaceUri, body.did, body.perms ?? "write", sa.issuer); return c.json({ ok: true }); }); -} -function buildAuthMiddleware(spaces: SpacesConfig): MiddlewareHandler | null { - if (!spaces.resolver) return null; - return createServiceAuthMiddleware({ - serviceDid: spaces.serviceDid as Did, - resolver: spaces.resolver, + app.post(`/xrpc/${SPACE}.admin.removeMember`, auth, async (c) => { + const sa = getAuth(c); + const body = (await c.req.json().catch(() => null)) as + | { spaceUri?: string; did?: string } + | null; + if (!body?.spaceUri || !body.did) { + return c.json({ error: "InvalidRequest", message: "spaceUri and did required" }, 400); + } + const space = await adapter.getSpace(body.spaceUri); + if (!space) return c.json({ error: "NotFound" }, 404); + if (space.ownerDid !== sa.issuer) { + return c.json({ error: "Forbidden", reason: "not-owner" }, 403); + } + if (body.did === space.ownerDid) { + return c.json({ error: "InvalidRequest", reason: "cannot-remove-owner" }, 400); + } + await adapter.removeMember(body.spaceUri, body.did); + return c.json({ ok: true }); }); } +function buildAuthMiddleware(spaces: SpacesConfig): MiddlewareHandler { + const verifier = buildVerifier(spaces); + return createServiceAuthMiddleware(verifier); +} + function getAuth(c: Parameters[0]): ServiceAuth { const auth = c.get("serviceAuth") as ServiceAuth | undefined; if (!auth) throw new Error("service auth not set"); @@ -319,8 +390,7 @@ function publicSpaceView(space: SpaceRow, forOwner: boolean) { memberListRef: space.memberListRef, appPolicyRef: space.appPolicyRef, createdAt: space.createdAt, - ...(forOwner ? { policy: space.policy, appPolicy: space.appPolicy } : {}), + ...(forOwner ? { appPolicy: space.appPolicy } : {}), }; } -export { resolveCollectionPolicy }; diff --git a/src/core/spaces/schema.ts b/src/core/spaces/schema.ts index 6899623..96fe02a 100644 --- a/src/core/spaces/schema.ts +++ b/src/core/spaces/schema.ts @@ -1,8 +1,15 @@ -import type { Database } from "../types"; +import type { ContrailConfig, Database } from "../types"; +import type { SqlDialect } from "../dialect"; import { getDialect } from "../dialect"; +import { + buildCollectionTables, + buildDynamicIndexes, + buildFtsTables, + buildCountColumns, +} from "../db/schema"; -export function buildSpacesSchema(db: Database): string[] { - const dialect = getDialect(db); +/** Spaces metadata tables — spaces, members, invites. No per-collection tables. */ +export function buildSpacesBaseSchema(dialect: SqlDialect): string[] { return [ `CREATE TABLE IF NOT EXISTS spaces ( uri TEXT PRIMARY KEY, @@ -12,7 +19,6 @@ export function buildSpacesSchema(db: Database): string[] { service_did TEXT NOT NULL, member_list_ref TEXT, app_policy_ref TEXT, - policy ${dialect.recordColumnType}, app_policy ${dialect.recordColumnType}, created_at ${dialect.bigintType} NOT NULL, deleted_at ${dialect.bigintType} @@ -20,19 +26,6 @@ export function buildSpacesSchema(db: Database): string[] { `CREATE INDEX IF NOT EXISTS idx_spaces_owner ON spaces(owner_did)`, `CREATE INDEX IF NOT EXISTS idx_spaces_type ON spaces(type)`, - `CREATE TABLE IF NOT EXISTS spaces_records ( - space_uri TEXT NOT NULL, - collection TEXT NOT NULL, - author_did TEXT NOT NULL, - rkey TEXT NOT NULL, - cid TEXT, - record ${dialect.recordColumnType}, - created_at ${dialect.bigintType} NOT NULL, - PRIMARY KEY (space_uri, collection, author_did, rkey) - )`, - `CREATE INDEX IF NOT EXISTS idx_spaces_records_space_col ON spaces_records(space_uri, collection, created_at DESC)`, - `CREATE INDEX IF NOT EXISTS idx_spaces_records_space_author ON spaces_records(space_uri, author_did, created_at DESC)`, - `CREATE TABLE IF NOT EXISTS spaces_members ( space_uri TEXT NOT NULL, did TEXT NOT NULL, @@ -59,7 +52,30 @@ export function buildSpacesSchema(db: Database): string[] { ]; } -export async function initSpacesSchema(db: Database): Promise { - const stmts = buildSpacesSchema(db); +/** Full spaces schema (base + per-collection tables + indexes). For callers + * that need a single array of statements. Note: this does NOT include FTS + * virtual tables or ALTER TABLE count columns — those must be applied with + * try/catch fallbacks and are handled by `initSchema`. */ +export function buildSpacesSchema(db: Database, config?: ContrailConfig): string[] { + const dialect = getDialect(db); + const base = buildSpacesBaseSchema(dialect); + if (!config) return base; + return [ + ...base, + ...buildCollectionTables(config, dialect, { forSpaces: true }), + ...buildDynamicIndexes(config, dialect, { forSpaces: true }), + ]; +} + +export async function initSpacesSchema(db: Database, config?: ContrailConfig): Promise { + const dialect = getDialect(db); + const stmts = buildSpacesSchema(db, config); await db.batch(stmts.map((s) => db.prepare(s))); + if (!config) return; + for (const stmt of buildFtsTables(config, dialect, { forSpaces: true })) { + try { await db.prepare(stmt).run(); } catch { /* ignore */ } + } + for (const stmt of buildCountColumns(config, { forSpaces: true })) { + try { await db.prepare(stmt).run(); } catch { /* ignore */ } + } } diff --git a/src/core/spaces/types.ts b/src/core/spaces/types.ts index d98bc34..7027c9b 100644 --- a/src/core/spaces/types.ts +++ b/src/core/spaces/types.ts @@ -1,13 +1,8 @@ import type { Database } from "../types"; import type { DidDocumentResolver } from "@atcute/identity-resolver"; -export type ReadMode = "member" | "member-own" | "owner"; -export type WriteMode = "member" | "owner"; - -export interface CollectionPolicy { - read: ReadMode; - write: WriteMode; -} +/** Member permission in a space. "write" implies "read"; owner is always implicit write. */ +export type MemberPerm = "read" | "write"; export type AppPolicyMode = "allow" | "deny"; @@ -21,13 +16,10 @@ export interface SpacesConfig { type: string; /** Service DID that service-auth tokens must target (aud claim). */ serviceDid: string; - /** Default per-collection policies. Spaces may override. */ - defaultPolicies?: Record; - /** Policy for collections that are not explicitly listed. Omit to reject. */ - defaultPolicy?: CollectionPolicy; /** Default app policy applied to new spaces. */ defaultAppPolicy?: AppPolicy; - /** DID document resolver for service-auth JWT verification. Required for production. */ + /** DID document resolver for service-auth JWT verification. + * Defaults to a composite PLC + did:web resolver if omitted. */ resolver?: DidDocumentResolver; } @@ -39,7 +31,6 @@ export interface SpaceRow { serviceDid: string; memberListRef: string | null; appPolicyRef: string | null; - policy: Record | null; appPolicy: AppPolicy | null; createdAt: number; deletedAt: number | null; @@ -48,7 +39,7 @@ export interface SpaceRow { export interface SpaceMemberRow { spaceUri: string; did: string; - perms: string; + perms: MemberPerm; addedAt: number; addedBy: string | null; } @@ -90,7 +81,7 @@ export interface CollectionCount { export interface InviteRow { tokenHash: string; spaceUri: string; - perms: string; + perms: MemberPerm; expiresAt: number | null; maxUses: number | null; usedCount: number; @@ -103,7 +94,7 @@ export interface InviteRow { export interface CreateInviteInput { spaceUri: string; tokenHash: string; - perms: string; + perms: MemberPerm; expiresAt: number | null; maxUses: number | null; createdBy: string; @@ -112,7 +103,7 @@ export interface CreateInviteInput { export interface RedeemInviteResult { spaceUri: string; - perms: string; + perms: MemberPerm; } export interface StorageAdapter { @@ -121,11 +112,10 @@ export interface StorageAdapter { getSpace(spaceUri: string): Promise; listSpaces(options: ListSpacesOptions): Promise<{ spaces: SpaceRow[]; cursor?: string }>; deleteSpace(spaceUri: string): Promise; - updateSpacePolicy(spaceUri: string, policy: Record): Promise; updateSpaceAppPolicy(spaceUri: string, appPolicy: AppPolicy): Promise; // Members - addMember(spaceUri: string, did: string, perms: string, addedBy: string | null): Promise; + addMember(spaceUri: string, did: string, perms: MemberPerm, addedBy: string | null): Promise; removeMember(spaceUri: string, did: string): Promise; getMember(spaceUri: string, did: string): Promise; listMembers(spaceUri: string): Promise; diff --git a/src/core/types.ts b/src/core/types.ts index 97e800b..219376a 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -21,6 +21,7 @@ export interface QueryableField { } export interface RelationConfig { + /** Short name of the child collection (a key in `collections`). */ collection: string; field?: string; match?: "uri" | "did"; @@ -35,6 +36,7 @@ export interface RelationConfig { /** A forward reference: this collection's records point at another collection. */ export interface ReferenceConfig { + /** Short name of the target collection. */ collection: string; /** Field on this collection's records containing the target URI. */ field: string; @@ -59,7 +61,9 @@ export type PipelineQueryHandler = ( ) => Promise; export interface FeedConfig { + /** Short name of the follow collection. */ follow: string; + /** Short names of target collections to fan out to. */ targets: string[]; /** Max feed items per user (default: 200). Oldest items are pruned after backfill. */ maxItems?: number; @@ -67,7 +71,17 @@ export interface FeedConfig { export const DEFAULT_FEED_MAX_ITEMS = 200; +export type CollectionMethod = "listRecords" | "getRecord"; +export const DEFAULT_COLLECTION_METHODS: CollectionMethod[] = [ + "listRecords", + "getRecord", +]; + export interface CollectionConfig { + /** Full NSID of the record type this collection indexes. */ + collection: string; + /** Include this collection in Jetstream ingest / discovery (default true). + * Set false for dependent collections (auto-fetched on demand). */ discover?: boolean; queryable?: Record; relations?: Record; @@ -77,22 +91,39 @@ export interface CollectionConfig { pipelineQueries?: Record; /** FTS5 search fields. Provide an array of field names to enable full-text search. Omit or set to false to disable. */ searchable?: string[] | false; + /** XRPC methods to emit. Defaults to ['listRecords', 'getRecord']. */ + methods?: CollectionMethod[]; + /** When spaces are enabled globally, emit a parallel spaces_records_ table + * so this collection can also live inside spaces. Defaults to true. */ + allowInSpaces?: boolean; } export interface ProfileConfig { + /** Full NSID of the profile record type. */ collection: string; + /** Short name used for table/endpoint naming. Defaults to the NSID's last segment. */ + shortName?: string; rkey?: string; // defaults to "self" } export const DEFAULT_PROFILES: ProfileConfig[] = [ - { collection: "app.bsky.actor.profile" }, + { collection: "app.bsky.actor.profile", shortName: "profile" }, ]; /** Normalize a profiles config entry (string or object) into ProfileConfig. */ export function normalizeProfileConfig( p: string | ProfileConfig ): ProfileConfig { - return typeof p === "string" ? { collection: p } : p; + if (typeof p === "string") { + return { collection: p, shortName: deriveShortName(p) }; + } + return { ...p, shortName: p.shortName ?? deriveShortName(p.collection) }; +} + +/** Last NSID segment, used as fallback short name. */ +export function deriveShortName(nsid: string): string { + const parts = nsid.split("."); + return parts[parts.length - 1] ?? nsid; } export const DEFAULT_JETSTREAMS = [ @@ -114,6 +145,8 @@ export interface Logger { export interface ContrailConfig { namespace: string; + /** Collections to index, keyed by short name. Short names become endpoint URL segments + * (`..listRecords`) and table suffixes (`records_`). */ collections: Record; profiles?: (string | ProfileConfig)[]; relays?: string[]; @@ -125,9 +158,32 @@ export interface ContrailConfig { notify?: boolean | string; /** Permissioned spaces configuration. When set, the service exposes space XRPCs. */ spaces?: import("./spaces/types").SpacesConfig; + /** Customize the auto-generated `.permissionSet` lexicon. */ + permissionSet?: PermissionSetConfig; +} + +/** Single entry in an atproto permission-set's `permissions` array. + * See https://atproto.com/guides/permission-sets for the full schema. */ +export type PermissionEntry = + | { type: "permission"; resource: "rpc"; lxm?: string[]; aud?: string; inheritAud?: boolean } + | { type: "permission"; resource: "repo"; collection?: string[] } + | { type: "permission"; resource: "blob"; accept?: string[]; maxSize?: number } + | { type: "permission"; resource: "account"; attr?: string[] } + | { type: "permission"; resource: string; [key: string]: unknown }; + +export interface PermissionSetConfig { + /** Shown on the OAuth consent screen. Defaults to the namespace. */ + title?: string; + /** Shown on the OAuth consent screen. Defaults to a generated description. */ + description?: string; + /** Extra permission entries appended after the auto-generated rpc entry — + * e.g. repo writes for collections your app needs the user to create, or + * blob permissions for uploads. */ + additional?: PermissionEntry[]; } export interface ResolvedRelation { + /** Short name of the child collection. */ collection: string; groupBy: string; groups: Record; // shortName → full token value @@ -136,6 +192,8 @@ export interface ResolvedRelation { export interface ResolvedMaps { queryable: Record>; relations: Record>; + /** Reverse map: full record NSID → short name. */ + nsidToShort: Record; } /** Config after resolveConfig() — has computed queryable/relation maps attached. */ @@ -152,19 +210,16 @@ export function resolveConfig(config: ContrailConfig): ResolvedContrailConfig { ); const collections = { ...config.collections }; for (const p of profiles) { - if (!collections[p.collection]) { - collections[p.collection] = { discover: false }; + const short = p.shortName!; + if (!collections[short]) { + collections[short] = { collection: p.collection, discover: false }; } } - // Auto-add follow collections from feed configs as dependent collections - if (config.feeds) { - for (const feed of Object.values(config.feeds)) { - if (!collections[feed.follow]) { - collections[feed.follow] = { discover: false }; - } - } - } + // Auto-add follow collections from feed configs as dependent collections if they're + // not already listed. Feed config already uses short names so nothing to resolve — + // but if the user forgot to declare the follow collection, we can't auto-add it without + // knowing its NSID. In that case we warn later via validateConfig. const base = { ...config, @@ -184,10 +239,13 @@ export function resolveConfig(config: ContrailConfig): ResolvedContrailConfig { function _resolveQueryableMaps(config: ContrailConfig): ResolvedMaps { const queryable: Record> = {}; const relations: Record> = {}; + const nsidToShort: Record = {}; + + for (const [short, colConfig] of Object.entries(config.collections)) { + nsidToShort[colConfig.collection] = short; - for (const [collection, colConfig] of Object.entries(config.collections)) { if (colConfig.queryable) { - queryable[collection] = colConfig.queryable; + queryable[short] = colConfig.queryable; } if (colConfig.relations) { @@ -195,8 +253,8 @@ function _resolveQueryableMaps(config: ContrailConfig): ResolvedMaps { if (!rel.groupBy) continue; const groups: Record = rel.groups ? { ...rel.groups } : {}; if (Object.keys(groups).length > 0) { - if (!relations[collection]) relations[collection] = {}; - relations[collection][relName] = { + if (!relations[short]) relations[short] = {}; + relations[short][relName] = { collection: rel.collection, groupBy: rel.groupBy, groups, @@ -206,31 +264,38 @@ function _resolveQueryableMaps(config: ContrailConfig): ResolvedMaps { } } - return { queryable, relations }; + return { queryable, relations, nsidToShort }; } -export function getFeedFollowCollections(config: ContrailConfig): string[] { +export function getFeedFollowShortNames(config: ContrailConfig): string[] { if (!config.feeds) return []; return [...new Set(Object.values(config.feeds).map((f) => f.follow))]; } +/** Alias for getFeedFollowShortNames. */ +export const getFeedFollowCollections = getFeedFollowShortNames; + // Record types export interface RecordRow { uri: string; did: string; - collection: string; + collection: string; // full NSID rkey: string; cid: string | null; record: string | null; time_us: number; indexed_at: number; + /** Set when the row originates from a per-space table. Used by the + * pipeline/hydration/response layers to route child queries to the same + * space and tag the output. */ + _space?: string; } export interface IngestEvent { uri: string; did: string; - collection: string; + collection: string; // full NSID rkey: string; operation: "create" | "update" | "delete"; cid: string | null; @@ -242,6 +307,7 @@ export interface IngestEvent { // Validation const SAFE_FIELD_NAME = /^[a-zA-Z0-9_.]+$/; +const SAFE_SHORT_NAME = /^[a-zA-Z][a-zA-Z0-9]*$/; export function validateFieldName(field: string): string { if (!SAFE_FIELD_NAME.test(field)) { @@ -250,8 +316,27 @@ export function validateFieldName(field: string): string { return field; } +function validateShortName(short: string): void { + if (!SAFE_SHORT_NAME.test(short)) { + throw new Error( + `Invalid collection short name: "${short}". Must be alphanumeric, starting with a letter.` + ); + } +} + export function validateConfig(config: ContrailConfig): void { - for (const [collection, colConfig] of Object.entries(config.collections)) { + const shortNames = new Set(); + for (const [short, colConfig] of Object.entries(config.collections)) { + validateShortName(short); + if (shortNames.has(short)) { + throw new Error(`Duplicate collection short name: ${short}`); + } + shortNames.add(short); + + if (!colConfig.collection) { + throw new Error(`Collection "${short}" is missing required 'collection' field (NSID)`); + } + for (const field of Object.keys(colConfig.queryable ?? {})) { validateFieldName(field); } @@ -259,6 +344,19 @@ export function validateConfig(config: ContrailConfig): void { if (rel.field) validateFieldName(rel.field); if (rel.groupBy) validateFieldName(rel.groupBy); if (rel.countDistinct) validateFieldName(rel.countDistinct); + if (!config.collections[rel.collection]) { + throw new Error( + `Relation in "${short}" references unknown collection short name "${rel.collection}"` + ); + } + } + for (const [, ref] of Object.entries(colConfig.references ?? {})) { + validateFieldName(ref.field); + if (!config.collections[ref.collection]) { + throw new Error( + `Reference in "${short}" references unknown collection short name "${ref.collection}"` + ); + } } if (Array.isArray(colConfig.searchable)) { for (const field of colConfig.searchable) { @@ -266,6 +364,23 @@ export function validateConfig(config: ContrailConfig): void { } } } + + if (config.feeds) { + for (const [feedName, feed] of Object.entries(config.feeds)) { + if (!config.collections[feed.follow]) { + throw new Error( + `Feed "${feedName}" references unknown follow collection "${feed.follow}"` + ); + } + for (const target of feed.targets) { + if (!config.collections[target]) { + throw new Error( + `Feed "${feedName}" references unknown target collection "${target}"` + ); + } + } + } + } } // Helpers @@ -285,26 +400,98 @@ export function getRelationField(rel: RelationConfig): string { return rel.field ?? DEFAULT_RELATION_FIELD; } -export function countColumnName(type: string): string { - return "count_" + type.replace(/[^a-zA-Z0-9]/g, "_"); +/** Sanitize a short name for use in SQL identifiers (already-validated; kept for paranoia). */ +function sanitizeIdentifier(name: string): string { + return name.replace(/[^a-zA-Z0-9]/g, "_"); +} + +/** Total-count column name for a relation targeting the given short name. */ +export function countColumnName(childShortName: string): string { + return "count_" + sanitizeIdentifier(childShortName); +} + +/** Grouped-count column name: `count__`. */ +export function groupedCountColumnName( + childShortName: string, + groupKey: string +): string { + return `count_${sanitizeIdentifier(childShortName)}_${sanitizeIdentifier(groupKey)}`; } -export function recordsTableName(collection: string): string { - return "records_" + collection.replace(/[^a-zA-Z0-9]/g, "_"); +/** Table name for a collection's records. */ +export function recordsTableName(shortName: string): string { + return "records_" + sanitizeIdentifier(shortName); } -export function getCollectionNames(config: ContrailConfig): string[] { +/** Table name for a collection's records inside spaces. */ +export function spacesRecordsTableName(shortName: string): string { + return "spaces_records_" + sanitizeIdentifier(shortName); +} + +/** All collection short names. */ +export function getCollectionShortNames(config: ContrailConfig): string[] { return Object.keys(config.collections); } -export function getDependentCollections(config: ContrailConfig): string[] { +/** Alias: collection short names (same as getCollectionShortNames). */ +export const getCollectionNames = getCollectionShortNames; + +/** All indexed record NSIDs (what Jetstream filters on). */ +export function getCollectionNsids(config: ContrailConfig): string[] { + return Object.values(config.collections).map((c) => c.collection); +} + +export function getDependentShortNames(config: ContrailConfig): string[] { return Object.entries(config.collections) .filter(([, c]) => c.discover === false) .map(([name]) => name); } -export function getDiscoverableCollections(config: ContrailConfig): string[] { +export function getDiscoverableShortNames(config: ContrailConfig): string[] { return Object.entries(config.collections) .filter(([, c]) => c.discover !== false) .map(([name]) => name); } + +/** Aliases for readability elsewhere. These return short names (new semantic). */ +export const getDependentCollections = getDependentShortNames; +export const getDiscoverableCollections = getDiscoverableShortNames; + +/** Short names of collections the user declared with `discover !== false`, mapped to NSIDs. */ +export function getDiscoverableNsids(config: ContrailConfig): string[] { + return Object.values(config.collections) + .filter((c) => c.discover !== false) + .map((c) => c.collection); +} + +export function getDependentNsids(config: ContrailConfig): string[] { + return Object.values(config.collections) + .filter((c) => c.discover === false) + .map((c) => c.collection); +} + +/** Short name for a record NSID, if known. */ +export function shortNameForNsid( + config: ContrailConfig, + nsid: string +): string | undefined { + const resolved = (config as ResolvedContrailConfig)._resolved; + if (resolved?.nsidToShort) return resolved.nsidToShort[nsid]; + for (const [short, c] of Object.entries(config.collections)) { + if (c.collection === nsid) return short; + } + return undefined; +} + +/** Full NSID for a collection short name. */ +export function nsidForShortName( + config: ContrailConfig, + short: string +): string | undefined { + return config.collections[short]?.collection; +} + +/** The methods a collection should expose via XRPC. */ +export function getCollectionMethods(cfg: CollectionConfig): CollectionMethod[] { + return cfg.methods ?? DEFAULT_COLLECTION_METHODS; +} diff --git a/src/generate.ts b/src/generate.ts index 140f27d..aa213d4 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -27,6 +27,28 @@ function fieldToParam(field: string): string { return field.replace(/\.(\w)/g, (_, c) => c.toUpperCase()); } +/** Locate the shipped space-template directory. Works in contrail's own repo + * and in downstream projects that depend on @atmo-dev/contrail. */ +function findSpaceTemplatesDir(rootDir: string): string | null { + const candidates = [ + join(rootDir, "spaces-lexicon-templates"), + join(rootDir, "node_modules/@atmo-dev/contrail/spaces-lexicon-templates"), + ]; + for (const p of candidates) { + if (existsSync(p)) return p; + } + return null; +} + +/** Yield all JSON files under a directory (recursive). */ +function* walkJson(dir: string): Generator { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) yield* walkJson(full); + else if (entry.isFile() && entry.name.endsWith(".json")) yield full; + } +} + interface QueryableField { type?: "range"; } @@ -153,6 +175,15 @@ export function generateLexicons(options: GenerateOptions): Record 0) { @@ -227,6 +267,15 @@ export function generateLexicons(options: GenerateOptions): Record f.targets))]; + const allTargetNsids = allTargets + .map((t) => config.collections[t]?.collection) + .filter((n): n is string => !!n); - // Merge queryable fields, relations, and references from all target collections const feedParams: Record = { feed: { type: "string", knownValues: feedNames, description: "Feed name" }, actor: { type: "string", format: "at-identifier", description: "DID or handle of the requesting user" }, - collection: { type: "string", knownValues: allTargets, description: "Filter by target collection (defaults to first target)" }, + collection: { type: "string", knownValues: allTargetNsids, description: "Filter by target collection (defaults to first target)" }, limit: { type: "integer", minimum: 1, maximum: 200, default: 50 }, cursor: { type: "string" }, profiles: { type: "boolean", description: "Include profile + identity info keyed by DID" }, @@ -326,8 +379,9 @@ export function generateLexicons(options: GenerateOptions): Record = {}; if (rel.groupBy) { - for (const token of getKnownValues(rel.collection, rel.groupBy)) { - const shortName = tokenShortName(token); - groupMapping[shortName] = token; - countFields.push({ name: `${relName}${cap(shortName)}Count`, description: `${relName} count where ${rel.groupBy} = ${shortName}` }); + for (const token of getKnownValues(relNsid, rel.groupBy)) { + const gShort = tokenShortName(token); + groupMapping[gShort] = token; + countFields.push({ name: `${relName}${cap(gShort)}Count`, description: `${relName} count where ${rel.groupBy} = ${gShort}` }); } } - relationDefs.push({ relName, collection: rel.collection, groupBy: rel.groupBy, groups: groupMapping }); + relationDefs.push({ relName, collection: relNsid, groupBy: rel.groupBy, groups: groupMapping }); } for (const [refName, ref] of Object.entries(targetConfig.references ?? {})) { - referenceDefs.push({ refName, collection: ref.collection }); + const refNsid = config.collections[ref.collection]?.collection ?? ref.collection; + referenceDefs.push({ refName, collection: refNsid }); } const defName = `feedRecord_${targetCol.replace(/[^a-zA-Z0-9]/g, "_")}`; @@ -469,13 +527,14 @@ export function generateLexicons(options: GenerateOptions): Record> = {}; const resolvedRelationsMap: Record }>> = {}; - for (const [collection, colConfig] of Object.entries(config.collections)) { + for (const [shortName, colConfig] of Object.entries(config.collections)) { + const collection = colConfig.collection; // full NSID for lexicon refs const collectionRef = getCollectionLexiconRef(collection); const autoDetected = detectQueryableFields(collection); const manual = colConfig.queryable ?? {}; const merged = { ...autoDetected, ...manual }; - resolvedQueryableMap[collection] = merged; + resolvedQueryableMap[shortName] = merged; // --- listRecords --- const listParams: Record = { @@ -483,6 +542,20 @@ export function generateLexicons(options: GenerateOptions): Record = {}; if (rel.groupBy) { - const knownValues = getKnownValues(rel.collection, rel.groupBy); + const knownValues = getKnownValues(relNsid, rel.groupBy); for (const token of knownValues) { - const shortName = tokenShortName(token); - groupMapping[shortName] = token; - countFields.push({ name: `${relName}${cap(shortName)}Count`, description: `${relName} count where ${rel.groupBy} = ${shortName}` }); - listParams[`${relName}${cap(shortName)}CountMin`] = { type: "integer", description: `Minimum ${relName} count where ${rel.groupBy} = ${shortName}` }; + const gShort = tokenShortName(token); + groupMapping[gShort] = token; + countFields.push({ name: `${relName}${cap(gShort)}Count`, description: `${relName} count where ${rel.groupBy} = ${gShort}` }); + listParams[`${relName}${cap(gShort)}CountMin`] = { type: "integer", description: `Minimum ${relName} count where ${rel.groupBy} = ${gShort}` }; } - if (!resolvedRelationsMap[collection]) resolvedRelationsMap[collection] = {}; - resolvedRelationsMap[collection][relName] = { collection: rel.collection, groupBy: rel.groupBy, groups: groupMapping }; + if (!resolvedRelationsMap[shortName]) resolvedRelationsMap[shortName] = {}; + resolvedRelationsMap[shortName][relName] = { collection: rel.collection, groupBy: rel.groupBy, groups: groupMapping }; } - relationDefs.push({ relName, collection: rel.collection, groupBy: rel.groupBy, groups: groupMapping }); + relationDefs.push({ relName, collection: relNsid, groupBy: rel.groupBy, groups: groupMapping }); } const referenceDefs: ReferenceDef[] = []; for (const [refName, ref] of Object.entries(colConfig.references ?? {})) { - referenceDefs.push({ refName, collection: ref.collection }); + const refNsid = config.collections[ref.collection]?.collection ?? ref.collection; + referenceDefs.push({ refName, collection: refNsid }); } for (const refName of Object.keys(colConfig.references ?? {})) { listParams[`hydrate${cap(refName)}`] = { type: "boolean", description: `Embed the referenced ${refName} record` }; @@ -547,19 +622,31 @@ export function generateLexicons(options: GenerateOptions): Record = { uri: { type: "string", format: "at-uri", description: "AT URI of the record" }, profiles: { type: "boolean", description: "Include profile + identity info keyed by DID" }, + ...(config.spaces + ? { + spaceUri: { + type: "string", + format: "at-uri", + description: "If set, fetch from this permissioned space (requires service-auth JWT).", + }, + } + : {}), }; for (const rd of relationDefs) { getParams[`hydrate${cap(rd.relName)}`] = { type: "integer", minimum: 1, maximum: 50, description: `Number of ${rd.relName} records to embed` }; @@ -568,26 +655,136 @@ export function generateLexicons(options: GenerateOptions): Record.space.* --- + + if (config.spaces) { + log("Generating space endpoints..."); + const templatesDir = findSpaceTemplatesDir(rootDir); + if (!templatesDir) { + log(" (space templates not found — skipping)"); + } else { + const templateIdRe = /^tools\.atmo\.space(\.[A-Za-z0-9.]+)?$/; + const idReplace = (id: string) => + id.startsWith("tools.atmo.space") ? id.replace(/^tools\.atmo\.space/, `${ns}.space`) : id; + + const rewriteRefs = (obj: any): any => { + if (Array.isArray(obj)) return obj.map(rewriteRefs); + if (obj && typeof obj === "object") { + const out: any = {}; + for (const [k, v] of Object.entries(obj)) { + if (k === "ref" && typeof v === "string" && v.startsWith("tools.atmo.space")) { + out[k] = v.replace(/^tools\.atmo\.space/, `${ns}.space`); + } else if (k === "id" && typeof v === "string" && templateIdRe.test(v)) { + out[k] = idReplace(v); + } else { + out[k] = rewriteRefs(v); + } + } + return out; + } + return obj; + }; + + for (const file of walkJson(templatesDir)) { + const doc = JSON.parse(readFileSync(file, "utf-8")); + if (typeof doc.id !== "string" || !templateIdRe.test(doc.id)) continue; + const newId = idReplace(doc.id); + const rewritten = rewriteRefs({ ...doc, id: newId }); + writeLexicon(newId, rewritten); + } + } + } + + // --- Permission set --- + // Permission-set lexicons (https://atproto.com/guides/permission-sets) can + // only reference NSIDs under the same namespace as the set itself, which + // matches what we emit here: everything under `.*`. + + { + log("Generating permission set..."); + const methodNsids: string[] = []; + for (const [nsid, doc] of Object.entries(generated)) { + const mainType = (doc as any)?.defs?.main?.type; + if (mainType === "query" || mainType === "procedure") { + methodNsids.push(nsid); + } + } + methodNsids.sort(); + + const psConfig = config.permissionSet ?? {}; + + // Permission-set lexicons can only reference NSIDs under their own namespace. + // Validate `additional` entries before we emit and produce an invalid schema. + const nsPrefix = `${ns}.`; + for (const [i, perm] of (psConfig.additional ?? []).entries()) { + const p = perm as { resource?: string; lxm?: string[]; collection?: string[] }; + const offending: string[] = []; + for (const nsid of p.lxm ?? []) { + if (nsid !== ns && !nsid.startsWith(nsPrefix)) offending.push(nsid); + } + for (const nsid of p.collection ?? []) { + if (nsid !== ns && !nsid.startsWith(nsPrefix)) offending.push(nsid); + } + if (offending.length > 0) { + throw new Error( + `permissionSet.additional[${i}] (${p.resource}) references NSIDs outside '${ns}': ` + + offending.join(", ") + + `. Permission-set lexicons can only reference NSIDs in their own namespace — ` + + `declare those as standalone scopes in your OAuth client config instead.` + ); + } + } + + writeLexicon(`${ns}.permissionSet`, { + lexicon: 1, + id: `${ns}.permissionSet`, + defs: { + main: { + type: "permission-set", + title: psConfig.title ?? ns, + description: + psConfig.description ?? `All XRPC methods exposed by the ${ns} service.`, + permissions: [ + { + type: "permission", + resource: "rpc", + // `aud: "*"` grants the user consent to call these methods on + // *any* service DID — so one consent covers dev (tunnel DID) and + // prod (published DID) without re-consenting. `inheritAud: true` + // would be correct if the include: scope carried an aud param, + // but consent UIs drop `?aud=*` on include: lines in practice. + aud: "*", + lxm: methodNsids, + }, + ...(psConfig.additional ?? []), + ], + }, + }, + }); + } + // --- Runtime files (only when called from script) --- if (options.writeRuntimeFiles) { // lex.config.js - const collectionNsids = Object.keys(config.collections); + const collectionNsids = Object.values(config.collections).map((c) => c.collection); const pulledFiles = [...scanLexiconsDir(lexiconDirs), ...scanLexiconsDir([])].flat(); const allRefs = new Set(); for (const file of pulledFiles) { diff --git a/src/index.ts b/src/index.ts index f6405ad..7eebb87 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,9 +33,7 @@ export type { PersistentIngestOptions } from "./core/persistent"; // Spaces export type { SpacesConfig, - CollectionPolicy, - ReadMode, - WriteMode, + MemberPerm, AppPolicy, AppPolicyMode, SpaceRow, diff --git a/src/lexicons.ts b/src/lexicons.ts deleted file mode 100644 index 7af9431..0000000 --- a/src/lexicons.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** Generated XRPC lexicon types for permissioned spaces (tools.atmo.space.*). - * These are the forever-stable transport contract — import them for type-safe - * clients and handlers against any contrail-backed space service. */ - -export * as ToolsAtmoSpaceDefs from "./lexicon-types/types/tools/atmo/space/defs.js"; -export * as ToolsAtmoSpaceGetSpace from "./lexicon-types/types/tools/atmo/space/getSpace.js"; -export * as ToolsAtmoSpaceListRecords from "./lexicon-types/types/tools/atmo/space/listRecords.js"; -export * as ToolsAtmoSpaceGetRecord from "./lexicon-types/types/tools/atmo/space/getRecord.js"; -export * as ToolsAtmoSpacePutRecord from "./lexicon-types/types/tools/atmo/space/putRecord.js"; -export * as ToolsAtmoSpaceAdminCreateSpace from "./lexicon-types/types/tools/atmo/space/admin/createSpace.js"; -export * as ToolsAtmoSpaceAdminAddMember from "./lexicon-types/types/tools/atmo/space/admin/addMember.js"; -export * as ToolsAtmoSpaceInviteCreate from "./lexicon-types/types/tools/atmo/space/invite/create.js"; -export * as ToolsAtmoSpaceInviteRedeem from "./lexicon-types/types/tools/atmo/space/invite/redeem.js"; -export * as ToolsAtmoSpaceInviteList from "./lexicon-types/types/tools/atmo/space/invite/list.js"; -export * as ToolsAtmoSpaceInviteRevoke from "./lexicon-types/types/tools/atmo/space/invite/revoke.js"; diff --git a/src/publish.ts b/src/publish.ts new file mode 100644 index 0000000..5c32df0 --- /dev/null +++ b/src/publish.ts @@ -0,0 +1,205 @@ +/** + * Publish locally-generated lexicons to a PDS as `com.atproto.lexicon.schema` + * records. Each lexicon ends up at `at:///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. + */ + +import { readFileSync, readdirSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { createInterface } from "node:readline/promises"; +import { stdin as input, stdout as output } from "node:process"; + +export interface Session { + did: string; + accessJwt: string; + pdsEndpoint: string; +} + +async function login(identifier: string, password: string): Promise { + const resolveRes = await fetch( + `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(identifier)}` + ); + let did: string; + if (resolveRes.ok) { + did = ((await resolveRes.json()) as { did: string }).did; + } else if (identifier.startsWith("did:")) { + did = identifier; + } else { + throw new Error(`Could not resolve handle: ${identifier}`); + } + + const docUrl = did.startsWith("did:plc:") + ? `https://plc.directory/${did}` + : did.startsWith("did:web:") + ? `https://${did.slice("did:web:".length)}/.well-known/did.json` + : null; + if (!docUrl) throw new Error(`Unsupported DID method: ${did}`); + + const docRes = await fetch(docUrl); + if (!docRes.ok) throw new Error(`Could not fetch DID doc for ${did}`); + const doc = (await docRes.json()) as { + service?: { id: string; type: string; serviceEndpoint: string }[]; + }; + const pds = doc.service?.find( + (s) => s.id.endsWith("#atproto_pds") || s.type === "AtprotoPersonalDataServer" + )?.serviceEndpoint; + if (!pds) throw new Error(`No PDS service entry in DID doc for ${did}`); + + const sessionRes = await fetch(`${pds}/xrpc/com.atproto.server.createSession`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ identifier, password }), + }); + if (!sessionRes.ok) { + throw new Error(`Login failed: ${sessionRes.status} ${await sessionRes.text()}`); + } + const session = (await sessionRes.json()) as { did: string; accessJwt: string }; + return { did: session.did, accessJwt: session.accessJwt, pdsEndpoint: pds }; +} + +function* walkJson(dir: string): Generator { + for (const name of readdirSync(dir)) { + const full = join(dir, name); + if (statSync(full).isDirectory()) yield* walkJson(full); + else if (name.endsWith(".json")) yield full; + } +} + +async function putLexiconRecord( + session: Session, + lexicon: { id: string; [k: string]: unknown } +): Promise { + const body = { + repo: session.did, + collection: "com.atproto.lexicon.schema", + rkey: lexicon.id, + record: { $type: "com.atproto.lexicon.schema", ...lexicon }, + }; + const res = await fetch(`${session.pdsEndpoint}/xrpc/com.atproto.repo.putRecord`, { + method: "POST", + headers: { + Authorization: `Bearer ${session.accessJwt}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + if (!res.ok) { + throw new Error(`putRecord failed for ${lexicon.id}: ${res.status} ${await res.text()}`); + } +} + +/** DNS authority an NSID resolves to: all segments except the final (name), + * read right-to-left. Example: `rsvp.atmo.space.deleteRecord` → `space.atmo.rsvp` + * (client looks up TXT at `_lexicon.space.atmo.rsvp`). Resolution does not walk + * up — each distinct authority needs its own TXT record. */ +export function nsidAuthority(nsid: string): string { + const parts = nsid.split("."); + if (parts.length < 2) return nsid; + return parts.slice(0, -1).reverse().join("."); +} + +/** Root DNS zone for an NSID — the first two segments reversed. */ +export function nsidRootDomain(nsid: string): string { + const parts = nsid.split("."); + if (parts.length < 2) return nsid; + return `${parts[1]}.${parts[0]}`; +} + +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; + /** Skip the interactive "do you control these zones?" prompt. */ + skipConfirm?: boolean; +} + +export async function publishLexicons(opts: PublishOptions): Promise<{ + published: number; + failed: string[]; + authorities: string[]; + session: Session; +}> { + const lexicons: { id: string; [k: string]: unknown }[] = []; + for (const file of walkJson(opts.generatedDir)) { + const doc = JSON.parse(readFileSync(file, "utf-8")); + if (typeof doc.id === "string") lexicons.push(doc); + } + lexicons.sort((a, b) => a.id.localeCompare(b.id)); + + const rootDomains = new Set(); + const authorities = new Set(); + for (const lex of lexicons) { + rootDomains.add(nsidRootDomain(lex.id)); + authorities.add(nsidAuthority(lex.id)); + } + const sortedAuthorities = [...authorities].sort(); + + console.log(`About to publish ${lexicons.length} lexicons from ${opts.generatedDir}.\n`); + console.log( + `⚠ Do you control ${rootDomains.size === 1 ? "the DNS zone" : "these DNS zones"} below?` + ); + for (const root of [...rootDomains].sort()) console.log(` ${root}`); + console.log( + "\n You'll need to add TXT records under that zone so clients can\n" + + " resolve the NSIDs. If you don't control it, the records will\n" + + " sit on your PDS but won't be authoritative.\n\n" + + " ⚠ Specifically: permission sets (OAuth `include:` scopes) will\n" + + " not work without DNS resolution — the user's PDS fetches the\n" + + " permission-set lexicon by resolving the NSID, and resolution\n" + + " requires a valid TXT record.\n" + ); + + if (!opts.skipConfirm) { + const rl = createInterface({ input, output }); + const answer = (await rl.question("Continue? [y/N] ")).trim().toLowerCase(); + rl.close(); + if (answer !== "y" && answer !== "yes") { + console.log("Aborted."); + return { published: 0, failed: [], authorities: sortedAuthorities, session: null as never }; + } + } + + console.log(`\nLogging in as ${opts.identifier}…`); + const session = await login(opts.identifier, opts.password); + console.log(` DID: ${session.did}`); + console.log(` PDS: ${session.pdsEndpoint}\n`); + console.log(`Publishing ${lexicons.length} lexicons…\n`); + + let ok = 0; + const failed: string[] = []; + for (const lex of lexicons) { + try { + await putLexiconRecord(session, lex); + console.log(` ✓ ${lex.id}`); + ok++; + } catch (err) { + console.error(` ✗ ${lex.id}: ${(err as Error).message}`); + failed.push(lex.id); + } + } + + console.log( + `\nPublished ${ok}/${lexicons.length} lexicons${failed.length > 0 ? ` (failures: ${failed.join(", ")})` : ""}.` + ); + + console.log("\n─────────────────────────────────────────────────"); + console.log("DNS TXT records 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" + ); + for (const authority of sortedAuthorities) { + console.log(` host: _lexicon.${authority}`); + console.log(` value: did=${session.did}`); + console.log(""); + } + console.log(`Total: ${sortedAuthorities.length} DNS TXT record(s) to add.`); + + return { published: ok, failed, authorities: sortedAuthorities, session }; +} diff --git a/tests/generate.test.ts b/tests/generate.test.ts index 0d2a0bf..8ab7a61 100644 --- a/tests/generate.test.ts +++ b/tests/generate.test.ts @@ -31,7 +31,8 @@ function generate(config: ContrailConfig) { const BASIC_CONFIG: ContrailConfig = { namespace: "test.app", collections: { - "com.example.post": { + post: { + collection: "com.example.post", queryable: { title: {}, body: {}, @@ -44,19 +45,21 @@ const BASIC_CONFIG: ContrailConfig = { const RELATIONS_CONFIG: ContrailConfig = { namespace: "test.app", collections: { - "com.example.post": { + post: { + collection: "com.example.post", queryable: { title: {} }, relations: { likes: { - collection: "com.example.like", + collection: "like", }, }, }, - "com.example.like": { + like: { + collection: "com.example.like", queryable: { status: {} }, references: { post: { - collection: "com.example.post", + collection: "post", field: "subject.uri", }, }, @@ -67,7 +70,8 @@ const RELATIONS_CONFIG: ContrailConfig = { const SEARCH_EXPLICIT_CONFIG: ContrailConfig = { namespace: "test.app", collections: { - "com.example.post": { + post: { + collection: "com.example.post", queryable: { title: {}, body: {}, @@ -82,7 +86,8 @@ const SEARCH_EXPLICIT_CONFIG: ContrailConfig = { const SEARCH_DISABLED_CONFIG: ContrailConfig = { namespace: "test.app", collections: { - "com.example.post": { + post: { + collection: "com.example.post", queryable: { title: {}, body: {} }, searchable: false, }, @@ -92,7 +97,8 @@ const SEARCH_DISABLED_CONFIG: ContrailConfig = { const SEARCH_AUTO_CONFIG: ContrailConfig = { namespace: "test.app", collections: { - "com.example.post": { + post: { + collection: "com.example.post", queryable: { title: {}, body: {}, @@ -139,41 +145,29 @@ describe("basic generation", () => { expect(output.properties.errors.type).toBe("array"); }); - it("generates listRecords with standard params", () => { - const params = getParams(lexicons["com.example.post.listRecords"]); + it("generates listRecords under ..listRecords", () => { + const params = getParams(lexicons["test.app.post.listRecords"]); expect(params.limit).toBeDefined(); expect(params.cursor).toBeDefined(); expect(params.actor).toBeDefined(); expect(params.profiles).toBeDefined(); - }); - - it("generates queryable field params", () => { - const params = getParams(lexicons["com.example.post.listRecords"]); expect(params.title).toBeDefined(); - expect(params.title.type).toBe("string"); - expect(params.body).toBeDefined(); + expect(params.bodyParam ?? params.body).toBeDefined(); expect(params.createdAtMin).toBeDefined(); expect(params.createdAtMax).toBeDefined(); }); - it("generates sort and order params", () => { - const params = getParams(lexicons["com.example.post.listRecords"]); - expect(params.sort).toBeDefined(); - expect(params.sort.knownValues).toContain("title"); - expect(params.sort.knownValues).toContain("body"); - expect(params.sort.knownValues).toContain("createdAt"); - expect(params.order.knownValues).toEqual(["asc", "desc"]); - }); - - it("generates getRecord with uri param", () => { - const params = getParams(lexicons["com.example.post.getRecord"]); + it("generates getRecord under ..getRecord", () => { + const lex = lexicons["test.app.post.getRecord"]; + expect(lex).toBeDefined(); + const params = getParams(lex); expect(params.uri).toBeDefined(); - expect(params.uri.format).toBe("at-uri"); + expect(params.uri.required ?? lex.defs.main.parameters.required).toContain("uri"); }); - it("does not include search on getRecord", () => { - const params = getParams(lexicons["com.example.post.getRecord"]); - expect(params.search).toBeUndefined(); + it("does not emit the old NSID-based endpoint paths", () => { + expect(lexicons["com.example.post.listRecords"]).toBeUndefined(); + expect(lexicons["com.example.post.getRecord"]).toBeUndefined(); }); }); @@ -184,69 +178,36 @@ describe("relations and references", () => { lexicons = generate(RELATIONS_CONFIG); }); - it("generates count filter params for relations", () => { - const params = getParams(lexicons["com.example.post.listRecords"]); + it("includes relation count params", () => { + const params = getParams(lexicons["test.app.post.listRecords"]); expect(params.likesCountMin).toBeDefined(); - expect(params.likesCountMin.type).toBe("integer"); - }); - - it("generates hydrate params for relations", () => { - const params = getParams(lexicons["com.example.post.listRecords"]); expect(params.hydrateLikes).toBeDefined(); - expect(params.hydrateLikes.type).toBe("integer"); }); - it("generates hydrate params for references", () => { - const params = getParams(lexicons["com.example.like.listRecords"]); + it("includes reference hydrate params on child", () => { + const params = getParams(lexicons["test.app.like.listRecords"]); expect(params.hydratePost).toBeDefined(); - expect(params.hydratePost.type).toBe("boolean"); - }); - - it("includes count fields in record def", () => { - const recordDef = lexicons["com.example.post.listRecords"].defs.record; - expect(recordDef.properties.likesCount).toBeDefined(); - expect(recordDef.properties.likesCount.type).toBe("integer"); - }); - - it("includes relation shape in record def (ungrouped → array)", () => { - const recordDef = lexicons["com.example.post.listRecords"].defs.record; - expect(recordDef.properties.likes).toBeDefined(); - expect(recordDef.properties.likes.type).toBe("array"); - }); - - it("includes reference shape in record def", () => { - const recordDef = lexicons["com.example.like.listRecords"].defs.record; - expect(recordDef.properties.post).toBeDefined(); - expect(recordDef.properties.post.type).toBe("ref"); - }); - - it("sort knownValues includes count fields", () => { - const params = getParams(lexicons["com.example.post.listRecords"]); - expect(params.sort.knownValues).toContain("likesCount"); }); }); describe("search: explicit fields", () => { let lexicons: Record; - beforeAll(() => { lexicons = generate(SEARCH_EXPLICIT_CONFIG); }); - it("includes search param listing only explicit fields", () => { - const params = getParams(lexicons["com.example.post.listRecords"]); + it("exposes search param", () => { + const params = getParams(lexicons["test.app.post.listRecords"]); expect(params.search).toBeDefined(); expect(params.search.description).toContain("title"); expect(params.search.description).toContain("body"); - expect(params.search.description).not.toContain("category"); - expect(params.search.description).not.toContain("createdAt"); }); }); describe("search: disabled", () => { it("does not include search param", () => { const lexicons = generate(SEARCH_DISABLED_CONFIG); - const params = getParams(lexicons["com.example.post.listRecords"]); + const params = getParams(lexicons["test.app.post.listRecords"]); expect(params.search).toBeUndefined(); }); }); @@ -254,7 +215,7 @@ describe("search: disabled", () => { describe("search: no searchable field configured", () => { it("does not include search param when searchable is omitted", () => { const lexicons = generate(SEARCH_AUTO_CONFIG); - const params = getParams(lexicons["com.example.post.listRecords"]); + const params = getParams(lexicons["test.app.post.listRecords"]); expect(params.search).toBeUndefined(); }); }); diff --git a/tests/helpers.ts b/tests/helpers.ts index 38a0a11..e41c753 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -1,7 +1,8 @@ import { createSqliteDatabase } from "../src/adapters/sqlite"; -import type { Database, ResolvedContrailConfig } from "../src/core/types"; +import type { Database, IngestEvent, ResolvedContrailConfig } from "../src/core/types"; import { resolveConfig } from "../src/core/types"; import { initSchema } from "../src/core/db/schema"; +import { applyEvents as coreApplyEvents, type ExistingRecordInfo } from "../src/core/db/records"; export function createTestDb(): Database { return createSqliteDatabase(":memory:"); @@ -10,7 +11,8 @@ export function createTestDb(): Database { export const TEST_CONFIG: ResolvedContrailConfig = resolveConfig({ namespace: "com.example", collections: { - "community.lexicon.calendar.event": { + event: { + collection: "community.lexicon.calendar.event", queryable: { mode: {}, name: {}, @@ -18,7 +20,7 @@ export const TEST_CONFIG: ResolvedContrailConfig = resolveConfig({ }, relations: { rsvps: { - collection: "community.lexicon.calendar.rsvp", + collection: "rsvp", groupBy: "status", groups: { interested: "community.lexicon.calendar.rsvp#interested", @@ -28,10 +30,11 @@ export const TEST_CONFIG: ResolvedContrailConfig = resolveConfig({ }, }, }, - "community.lexicon.calendar.rsvp": { + rsvp: { + collection: "community.lexicon.calendar.rsvp", references: { event: { - collection: "community.lexicon.calendar.event", + collection: "event", field: "subject.uri", }, }, @@ -45,6 +48,19 @@ export async function createTestDbWithSchema(): Promise { return db; } +/** Apply events with TEST_CONFIG baked in — avoids each test having to pass it. */ +export function applyEvents( + db: Database, + events: IngestEvent[], + options?: { + skipReplayDetection?: boolean; + skipFeedFanout?: boolean; + existing?: Map; + } +): Promise { + return coreApplyEvents(db, events, TEST_CONFIG, options); +} + export function makeEvent(overrides: Partial<{ uri: string; did: string; diff --git a/tests/hydrate.test.ts b/tests/hydrate.test.ts index 5dc95de..eb174b5 100644 --- a/tests/hydrate.test.ts +++ b/tests/hydrate.test.ts @@ -3,15 +3,15 @@ import type { Database, RecordRow, RelationConfig, ReferenceConfig } from "../sr import { recordsTableName } from "../src/core/types"; import { parseHydrateParams, resolveHydrates, resolveReferences } from "../src/core/router/hydrate"; import { createTestDbWithSchema, makeEvent, TEST_CONFIG } from "./helpers"; -import { applyEvents } from "../src/core/db/records"; +import { applyEvents } from "./helpers"; describe("parseHydrateParams", () => { const relations: Record = { - rsvps: { collection: "community.lexicon.calendar.rsvp", groupBy: "status" }, + rsvps: { collection: "rsvp", groupBy: "status" }, comments: { collection: "test.comment" }, }; const references: Record = { - event: { collection: "community.lexicon.calendar.event", field: "subject.uri" }, + event: { collection: "event", field: "subject.uri" }, }; it("parses valid relation hydrate params", () => { @@ -74,7 +74,7 @@ describe("resolveHydrates", () => { }); it("returns empty when no records", async () => { - const relations = TEST_CONFIG.collections["community.lexicon.calendar.event"].relations!; + const relations = TEST_CONFIG.collections["event"].relations!; const result = await resolveHydrates(db, relations, { rsvps: 5 }, []); expect(result).toEqual({}); }); @@ -91,7 +91,7 @@ describe("resolveHydrates", () => { makeEvent({ uri: `at://did:plc:user${i}/community.lexicon.calendar.rsvp/r${i}`, did: `did:plc:user${i}`, - collection: "community.lexicon.calendar.rsvp", + collection: "rsvp", rkey: `r${i}`, record: { subject: { uri: eventUri }, status: "going" }, time_us: 2000 + i, @@ -100,11 +100,11 @@ describe("resolveHydrates", () => { } const eventRow = await db - .prepare(`SELECT * FROM ${recordsTableName("community.lexicon.calendar.event")} WHERE uri = ?`) + .prepare(`SELECT * FROM ${recordsTableName("event")} WHERE uri = ?`) .bind(eventUri) .first(); - const relations = TEST_CONFIG.collections["community.lexicon.calendar.event"].relations!; + const relations = TEST_CONFIG.collections["event"].relations!; const result = await resolveHydrates(db, relations, { rsvps: 10 }, [eventRow!]); expect(result[eventUri]).toBeDefined(); @@ -121,7 +121,7 @@ describe("resolveHydrates", () => { makeEvent({ uri: `at://did:plc:user${i}/community.lexicon.calendar.rsvp/r${i}`, did: `did:plc:user${i}`, - collection: "community.lexicon.calendar.rsvp", + collection: "rsvp", rkey: `r${i}`, record: { subject: { uri: eventUri }, status: "going" }, time_us: 2000 + i, @@ -130,11 +130,11 @@ describe("resolveHydrates", () => { } const eventRow = await db - .prepare(`SELECT * FROM ${recordsTableName("community.lexicon.calendar.event")} WHERE uri = ?`) + .prepare(`SELECT * FROM ${recordsTableName("event")} WHERE uri = ?`) .bind(eventUri) .first(); - const relations = TEST_CONFIG.collections["community.lexicon.calendar.event"].relations!; + const relations = TEST_CONFIG.collections["event"].relations!; const result = await resolveHydrates(db, relations, { rsvps: 2 }, [eventRow!]); expect(result[eventUri].rsvps["going"].length).toBeLessThanOrEqual(2); @@ -152,7 +152,7 @@ describe("resolveHydrates", () => { makeEvent({ uri: `at://did:plc:other/community.lexicon.calendar.rsvp/r1`, did: "did:plc:other", - collection: "community.lexicon.calendar.rsvp", + collection: "rsvp", rkey: "r1", record: { author: did, status: "going" }, time_us: 2000, @@ -160,13 +160,13 @@ describe("resolveHydrates", () => { ]); const eventRow = await db - .prepare(`SELECT * FROM ${recordsTableName("community.lexicon.calendar.event")} WHERE uri = ?`) + .prepare(`SELECT * FROM ${recordsTableName("event")} WHERE uri = ?`) .bind(eventUri) .first(); // match: "did" means matchValues are parent DIDs, and field points to where the DID is stored const relations: Record = { - rsvps: { collection: "community.lexicon.calendar.rsvp", match: "did", field: "author", groupBy: "status" }, + rsvps: { collection: "rsvp", match: "did", field: "author", groupBy: "status" }, }; const result = await resolveHydrates(db, relations, { rsvps: 10 }, [eventRow!]); @@ -189,7 +189,7 @@ describe("resolveHydrates", () => { makeEvent({ uri: rsvpUri, did: "did:plc:user1", - collection: "community.lexicon.calendar.rsvp", + collection: "rsvp", rkey: "r1", record: { subject: { uri: eventUri }, status: "going" }, time_us: 2000, @@ -197,11 +197,11 @@ describe("resolveHydrates", () => { ]); const rsvpRow = await db - .prepare(`SELECT * FROM ${recordsTableName("community.lexicon.calendar.rsvp")} WHERE uri = ?`) + .prepare(`SELECT * FROM ${recordsTableName("rsvp")} WHERE uri = ?`) .bind(rsvpUri) .first(); - const references = TEST_CONFIG.collections["community.lexicon.calendar.rsvp"].references!; + const references = TEST_CONFIG.collections["rsvp"].references!; const result = await resolveReferences(db, references, new Set(["event"]), [rsvpRow!]); expect(result[rsvpUri]).toBeDefined(); @@ -227,7 +227,7 @@ describe("resolveHydrates", () => { makeEvent({ uri: rsvpUri1, did: "did:plc:user1", - collection: "community.lexicon.calendar.rsvp", + collection: "rsvp", rkey: "r1", record: { subject: { uri: eventUri1 }, status: "going" }, time_us: 2000, @@ -235,7 +235,7 @@ describe("resolveHydrates", () => { makeEvent({ uri: rsvpUri2, did: "did:plc:user2", - collection: "community.lexicon.calendar.rsvp", + collection: "rsvp", rkey: "r2", record: { subject: { uri: eventUri2 }, status: "interested" }, time_us: 2001, @@ -243,10 +243,10 @@ describe("resolveHydrates", () => { ]); const rsvpRows = await db - .prepare(`SELECT * FROM ${recordsTableName("community.lexicon.calendar.rsvp")} ORDER BY time_us DESC`) + .prepare(`SELECT * FROM ${recordsTableName("rsvp")} ORDER BY time_us DESC`) .all(); - const references = TEST_CONFIG.collections["community.lexicon.calendar.rsvp"].references!; + const references = TEST_CONFIG.collections["rsvp"].references!; const result = await resolveReferences(db, references, new Set(["event"]), rsvpRows.results!); // Each RSVP should have its event resolved @@ -266,7 +266,7 @@ describe("resolveHydrates", () => { makeEvent({ uri: "at://did:plc:user1/community.lexicon.calendar.rsvp/r1", did: "did:plc:user1", - collection: "community.lexicon.calendar.rsvp", + collection: "rsvp", rkey: "r1", record: { subject: { uri: eventUri } }, time_us: 2000, @@ -274,11 +274,11 @@ describe("resolveHydrates", () => { ]); const eventRow = await db - .prepare(`SELECT * FROM ${recordsTableName("community.lexicon.calendar.event")} WHERE uri = ?`) + .prepare(`SELECT * FROM ${recordsTableName("event")} WHERE uri = ?`) .bind(eventUri) .first(); - const relations = TEST_CONFIG.collections["community.lexicon.calendar.event"].relations!; + const relations = TEST_CONFIG.collections["event"].relations!; const result = await resolveHydrates(db, relations, { rsvps: 10 }, [eventRow!]); expect(result[eventUri].rsvps).toBeDefined(); diff --git a/tests/notify.test.ts b/tests/notify.test.ts index 6a68c66..2477dee 100644 --- a/tests/notify.test.ts +++ b/tests/notify.test.ts @@ -1,9 +1,9 @@ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; import type { Database } from "../src/core/types"; -import { createTestDbWithSchema, makeEvent, TEST_CONFIG } from "./helpers"; +import { applyEvents, createTestDbWithSchema, makeEvent, TEST_CONFIG } from "./helpers"; import { parseAtUri } from "../src/core/router/notify"; import { createApp } from "../src/core/router/index"; -import { applyEvents, queryRecords } from "../src/core/db/records"; +import { queryRecords } from "../src/core/db/records"; import type { Hono } from "hono"; const NOTIFY_CONFIG = { ...TEST_CONFIG, notify: true }; @@ -175,7 +175,7 @@ describe("POST notifyOfUpdate", () => { // Pre-populate a record await applyEvents(db, [ makeEvent({ uri, did, rkey: "evt1", record: { name: "Old" } }), - ]); + ], TEST_CONFIG); await seedIdentity(did, "https://pds.example.com"); mockFetch({}); // PDS returns 404 for everything @@ -287,7 +287,7 @@ describe("POST notifyOfUpdate", () => { }); expect(result.records).toHaveLength(1); expect( - result.records[0].counts?.["community.lexicon.calendar.rsvp"] + result.records[0].counts?.["rsvp"] ).toBe(1); }); @@ -321,7 +321,7 @@ describe("POST notifyOfUpdate", () => { let result = await queryRecords(db, TEST_CONFIG, { collection: "community.lexicon.calendar.event", }); - expect(result.records[0].counts?.["community.lexicon.calendar.rsvp"]).toBe(1); + expect(result.records[0].counts?.["rsvp"]).toBe(1); // Now notify with the same RSVP (same CID) — should be a no-op await seedIdentity(did, "https://pds.example.com"); @@ -343,7 +343,7 @@ describe("POST notifyOfUpdate", () => { result = await queryRecords(db, TEST_CONFIG, { collection: "community.lexicon.calendar.event", }); - expect(result.records[0].counts?.["community.lexicon.calendar.rsvp"]).toBe(1); + expect(result.records[0].counts?.["rsvp"]).toBe(1); }); it("uses update (not create) when record exists with different CID", async () => { @@ -400,7 +400,7 @@ describe("POST notifyOfUpdate", () => { const result = await queryRecords(db, TEST_CONFIG, { collection: "community.lexicon.calendar.event", }); - expect(result.records[0].counts?.["community.lexicon.calendar.rsvp"]).toBe(1); + expect(result.records[0].counts?.["rsvp"]).toBe(1); }); it("does nothing when record not on PDS and not local", async () => { @@ -449,7 +449,7 @@ describe("POST notifyOfUpdate", () => { let result = await queryRecords(db, TEST_CONFIG, { collection: "community.lexicon.calendar.event", }); - expect(result.records[0].counts?.["community.lexicon.calendar.rsvp"]).toBe(1); + expect(result.records[0].counts?.["rsvp"]).toBe(1); // Notify with RSVP URI — PDS returns 404 (deleted) await seedIdentity(did, "https://pds.example.com"); @@ -468,6 +468,6 @@ describe("POST notifyOfUpdate", () => { result = await queryRecords(db, TEST_CONFIG, { collection: "community.lexicon.calendar.event", }); - expect(result.records[0].counts?.["community.lexicon.calendar.rsvp"] ?? 0).toBe(0); + expect(result.records[0].counts?.["rsvp"] ?? 0).toBe(0); }); }); diff --git a/tests/records.test.ts b/tests/records.test.ts index 7e3e401..893b1d0 100644 --- a/tests/records.test.ts +++ b/tests/records.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach } from "vitest"; import type { Database } from "../src/core/types"; -import { createTestDbWithSchema, makeEvent, TEST_CONFIG } from "./helpers"; -import { applyEvents, queryRecords, getLastCursor, saveCursor } from "../src/core/db/records"; +import { applyEvents, createTestDbWithSchema, makeEvent, TEST_CONFIG } from "./helpers"; +import { queryRecords, getLastCursor, saveCursor } from "../src/core/db/records"; let db: Database; @@ -97,7 +97,7 @@ describe("applyEvents", () => { }); expect(result.records).toHaveLength(1); expect(result.records[0].counts).toBeDefined(); - expect(result.records[0].counts!["community.lexicon.calendar.rsvp"]).toBe(1); + expect(result.records[0].counts!["rsvp"]).toBe(1); }); it("decrements counts on delete", async () => { diff --git a/tests/schema.test.ts b/tests/schema.test.ts index 54829ff..6197818 100644 --- a/tests/schema.test.ts +++ b/tests/schema.test.ts @@ -12,8 +12,8 @@ describe("initSchema", () => { .all<{ name: string }>(); const names = tables.results.map((t) => t.name); - expect(names).toContain("records_community_lexicon_calendar_event"); - expect(names).toContain("records_community_lexicon_calendar_rsvp"); + expect(names).toContain("records_event"); + expect(names).toContain("records_rsvp"); expect(names).toContain("backfills"); expect(names).toContain("discovery"); expect(names).toContain("cursor"); diff --git a/tests/spaces-acl.test.ts b/tests/spaces-acl.test.ts index c01a2c1..81ff32a 100644 --- a/tests/spaces-acl.test.ts +++ b/tests/spaces-acl.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; -import { checkAccess, resolveCollectionPolicy } from "../src/core/spaces/acl"; -import type { SpaceRow, SpaceMemberRow, SpacesConfig } from "../src/core/spaces/types"; +import { checkAccess } from "../src/core/spaces/acl"; +import type { SpaceMemberRow, SpaceRow } from "../src/core/spaces/types"; function mkSpace(overrides: Partial = {}): SpaceRow { return { @@ -11,7 +11,6 @@ function mkSpace(overrides: Partial = {}): SpaceRow { serviceDid: "did:web:example.com#svc", memberListRef: null, appPolicyRef: null, - policy: null, appPolicy: null, createdAt: 1, deletedAt: null, @@ -19,150 +18,149 @@ function mkSpace(overrides: Partial = {}): SpaceRow { }; } -function mkMember(did: string): SpaceMemberRow { - return { spaceUri: "x", did, perms: "member", addedAt: 1, addedBy: null }; +function mkMember(did: string, perms: "read" | "write" = "write"): SpaceMemberRow { + return { spaceUri: "x", did, perms, addedAt: 1, addedBy: null }; } -const cfg: Pick = { - defaultPolicies: { - "app.event.location": { read: "member", write: "owner" }, - "app.event.message": { read: "member", write: "member" }, - "app.event.intake": { read: "owner", write: "member" }, - "app.event.ticket": { read: "member-own", write: "owner" }, - }, -}; - describe("spaces acl", () => { - it("denies when no policy resolves", () => { - const s = mkSpace(); - const r = checkAccess({ - op: "read", collection: "unknown.ns", space: s, - callerDid: "did:plc:alice", member: null, config: {}, - }); - expect(r.allow).toBe(false); - expect((r as any).reason).toBe("no-policy"); - }); - - it("falls back from space policy → defaultPolicies → defaultPolicy", () => { - const s = mkSpace({ policy: { "override.ns": { read: "member", write: "owner" } } }); - expect(resolveCollectionPolicy(s, "override.ns", cfg)?.read).toBe("member"); - expect(resolveCollectionPolicy(s, "app.event.message", cfg)?.read).toBe("member"); - expect(resolveCollectionPolicy(s, "totally.new", { ...cfg, defaultPolicy: { read: "owner", write: "owner" }})?.read).toBe("owner"); - }); - - it("owner can read/write/delete everything, even without a member row", () => { + it("owner can read/write/delete without a member row", () => { const s = mkSpace(); for (const op of ["read", "write", "delete"] as const) { const r = checkAccess({ - op, collection: "app.event.location", space: s, - callerDid: "did:plc:alice", member: null, config: cfg, + op, + space: s, + callerDid: "did:plc:alice", + member: null, }); expect(r.allow).toBe(true); } }); - it("member read: members allowed, non-members denied", () => { + it("non-member cannot read", () => { const s = mkSpace(); - const bob = "did:plc:bob"; - const denied = checkAccess({ - op: "read", collection: "app.event.message", space: s, - callerDid: bob, member: null, config: cfg, - }); - expect(denied.allow).toBe(false); - expect((denied as any).reason).toBe("not-member"); - - const allowed = checkAccess({ - op: "read", collection: "app.event.message", space: s, - callerDid: bob, member: mkMember(bob), config: cfg, + const r = checkAccess({ + op: "read", + space: s, + callerDid: "did:plc:bob", + member: null, }); - expect(allowed.allow).toBe(true); + expect(r.allow).toBe(false); + expect((r as any).reason).toBe("not-member"); }); - it("owner-write: member cannot write, owner can", () => { + it("member with read perm can read", () => { const s = mkSpace(); - const bob = "did:plc:bob"; - const memberTry = checkAccess({ - op: "write", collection: "app.event.location", space: s, - callerDid: bob, member: mkMember(bob), config: cfg, - }); - expect(memberTry.allow).toBe(false); - expect((memberTry as any).reason).toBe("not-owner"); - - const ownerTry = checkAccess({ - op: "write", collection: "app.event.location", space: s, - callerDid: "did:plc:alice", member: null, config: cfg, + const r = checkAccess({ + op: "read", + space: s, + callerDid: "did:plc:bob", + member: mkMember("did:plc:bob", "read"), }); - expect(ownerTry.allow).toBe(true); + expect(r.allow).toBe(true); }); - it("member-own read: member can read own, not others'", () => { + it("member with read perm cannot write", () => { const s = mkSpace(); - const bob = "did:plc:bob"; - const own = checkAccess({ - op: "read", collection: "app.event.ticket", space: s, - callerDid: bob, member: mkMember(bob), targetAuthorDid: bob, config: cfg, + const r = checkAccess({ + op: "write", + space: s, + callerDid: "did:plc:bob", + member: mkMember("did:plc:bob", "read"), }); - expect(own.allow).toBe(true); + expect(r.allow).toBe(false); + expect((r as any).reason).toBe("not-writer"); + }); - const other = checkAccess({ - op: "read", collection: "app.event.ticket", space: s, - callerDid: bob, member: mkMember(bob), targetAuthorDid: "did:plc:charlie", config: cfg, + it("member with write perm can write", () => { + const s = mkSpace(); + const r = checkAccess({ + op: "write", + space: s, + callerDid: "did:plc:bob", + member: mkMember("did:plc:bob", "write"), }); - expect(other.allow).toBe(false); - expect((other as any).reason).toBe("not-own-record"); + expect(r.allow).toBe(true); }); - it("owner-read: only owner reads intake answers", () => { + it("non-member cannot write", () => { const s = mkSpace(); - const bob = "did:plc:bob"; - const memberTry = checkAccess({ - op: "read", collection: "app.event.intake", space: s, - callerDid: bob, member: mkMember(bob), config: cfg, + const r = checkAccess({ + op: "write", + space: s, + callerDid: "did:plc:bob", + member: null, }); - expect(memberTry.allow).toBe(false); - expect((memberTry as any).reason).toBe("not-owner"); + expect(r.allow).toBe(false); + expect((r as any).reason).toBe("not-writer"); + }); - const ownerTry = checkAccess({ - op: "read", collection: "app.event.intake", space: s, - callerDid: "did:plc:alice", member: null, config: cfg, + it("delete own: member-with-write can delete own record", () => { + const s = mkSpace(); + const r = checkAccess({ + op: "delete", + space: s, + callerDid: "did:plc:bob", + member: mkMember("did:plc:bob", "write"), + targetAuthorDid: "did:plc:bob", }); - expect(ownerTry.allow).toBe(true); + expect(r.allow).toBe(true); }); - it("delete: author can delete own, owner can delete any, stranger denied", () => { + it("delete other's: member-with-write cannot delete someone else's record", () => { const s = mkSpace(); - const bob = "did:plc:bob"; - const authorOwn = checkAccess({ - op: "delete", collection: "app.event.message", space: s, - callerDid: bob, member: mkMember(bob), targetAuthorDid: bob, config: cfg, + const r = checkAccess({ + op: "delete", + space: s, + callerDid: "did:plc:bob", + member: mkMember("did:plc:bob", "write"), + targetAuthorDid: "did:plc:charlie", }); - expect(authorOwn.allow).toBe(true); + expect(r.allow).toBe(false); + expect((r as any).reason).toBe("not-own-record"); + }); - const ownerDeleteAny = checkAccess({ - op: "delete", collection: "app.event.message", space: s, - callerDid: "did:plc:alice", member: null, targetAuthorDid: bob, config: cfg, + it("delete any: owner can delete anyone's record", () => { + const s = mkSpace(); + const r = checkAccess({ + op: "delete", + space: s, + callerDid: "did:plc:alice", + member: null, + targetAuthorDid: "did:plc:bob", }); - expect(ownerDeleteAny.allow).toBe(true); + expect(r.allow).toBe(true); + }); - const otherMember = checkAccess({ - op: "delete", collection: "app.event.message", space: s, - callerDid: "did:plc:charlie", member: mkMember("did:plc:charlie"), targetAuthorDid: bob, config: cfg, + it("delete without write perm: read-only member cannot delete own", () => { + const s = mkSpace(); + const r = checkAccess({ + op: "delete", + space: s, + callerDid: "did:plc:bob", + member: mkMember("did:plc:bob", "read"), + targetAuthorDid: "did:plc:bob", }); - expect(otherMember.allow).toBe(false); - expect((otherMember as any).reason).toBe("not-own-record"); + expect(r.allow).toBe(false); + expect((r as any).reason).toBe("not-writer"); }); it("app policy: allow-mode with apps[] denylists those apps", () => { const s = mkSpace({ appPolicy: { mode: "allow", apps: ["blocked.app"] } }); const ok = checkAccess({ - op: "read", collection: "app.event.message", space: s, - callerDid: "did:plc:alice", member: null, clientId: "fine.app", config: cfg, + op: "read", + space: s, + callerDid: "did:plc:alice", + member: null, + clientId: "fine.app", }); expect(ok.allow).toBe(true); const blocked = checkAccess({ - op: "read", collection: "app.event.message", space: s, - callerDid: "did:plc:alice", member: null, clientId: "blocked.app", config: cfg, + op: "read", + space: s, + callerDid: "did:plc:alice", + member: null, + clientId: "blocked.app", }); expect(blocked.allow).toBe(false); expect((blocked as any).reason).toBe("app-not-allowed"); @@ -171,14 +169,20 @@ describe("spaces acl", () => { it("app policy: deny-mode with apps[] allowlists those apps", () => { const s = mkSpace({ appPolicy: { mode: "deny", apps: ["trusted.app"] } }); const ok = checkAccess({ - op: "read", collection: "app.event.message", space: s, - callerDid: "did:plc:alice", member: null, clientId: "trusted.app", config: cfg, + op: "read", + space: s, + callerDid: "did:plc:alice", + member: null, + clientId: "trusted.app", }); expect(ok.allow).toBe(true); const blocked = checkAccess({ - op: "read", collection: "app.event.message", space: s, - callerDid: "did:plc:alice", member: null, clientId: "anon.app", config: cfg, + op: "read", + space: s, + callerDid: "did:plc:alice", + member: null, + clientId: "anon.app", }); expect(blocked.allow).toBe(false); expect((blocked as any).reason).toBe("app-not-allowed"); diff --git a/tests/spaces-e2e.test.ts b/tests/spaces-e2e.test.ts index c49107f..fbef767 100644 --- a/tests/spaces-e2e.test.ts +++ b/tests/spaces-e2e.test.ts @@ -13,15 +13,14 @@ const CHARLIE = "did:plc:charlie"; const CONFIG: ContrailConfig = { namespace: "test.spaces", - collections: {}, + collections: { + location: { collection: "app.event.location" }, + message: { collection: "app.event.message" }, + ticket: { collection: "app.event.ticket" }, + }, spaces: { type: "tools.atmo.event.space", serviceDid: "did:web:test.example#svc", - defaultPolicies: { - "app.event.location": { read: "member", write: "owner" }, - "app.event.message": { read: "member", write: "member" }, - "app.event.ticket": { read: "member-own", write: "owner" }, - }, }, }; @@ -91,7 +90,7 @@ describe("spaces e2e", () => { app = await makeApp(); // Alice creates a space - const res = await call(app, "POST", "/xrpc/tools.atmo.space.admin.createSpace", ALICE, { + const res = await call(app, "POST", "/xrpc/test.spaces.space.admin.createSpace", ALICE, { key: "birthday-2026", }); expect(res.status).toBe(200); @@ -101,7 +100,7 @@ describe("spaces e2e", () => { }); it("owner can write a location record", async () => { - const res = await call(app, "POST", "/xrpc/tools.atmo.space.putRecord", ALICE, { + const res = await call(app, "POST", "/xrpc/test.spaces.space.putRecord", ALICE, { spaceUri, collection: "app.event.location", record: { address: "123 Main St" }, @@ -116,7 +115,7 @@ describe("spaces e2e", () => { const res = await call( app, "GET", - `/xrpc/tools.atmo.space.listRecords?spaceUri=${encodeURIComponent(spaceUri)}&collection=app.event.location`, + `/xrpc/test.spaces.space.listRecords?spaceUri=${encodeURIComponent(spaceUri)}&collection=app.event.location`, BOB ); expect(res.status).toBe(403); @@ -125,7 +124,7 @@ describe("spaces e2e", () => { }); it("non-member cannot write a message", async () => { - const res = await call(app, "POST", "/xrpc/tools.atmo.space.putRecord", BOB, { + const res = await call(app, "POST", "/xrpc/test.spaces.space.putRecord", BOB, { spaceUri, collection: "app.event.message", record: { text: "spam" }, @@ -134,10 +133,10 @@ describe("spaces e2e", () => { }); it("owner adds Bob as member", async () => { - const res = await call(app, "POST", "/xrpc/tools.atmo.space.admin.addMember", ALICE, { + const res = await call(app, "POST", "/xrpc/test.spaces.space.admin.addMember", ALICE, { spaceUri, did: BOB, - perms: "attendee", + perms: "write", }); expect(res.status).toBe(200); }); @@ -146,7 +145,7 @@ describe("spaces e2e", () => { const res = await call( app, "GET", - `/xrpc/tools.atmo.space.listRecords?spaceUri=${encodeURIComponent(spaceUri)}&collection=app.event.location`, + `/xrpc/test.spaces.space.listRecords?spaceUri=${encodeURIComponent(spaceUri)}&collection=app.event.location`, BOB ); expect(res.status).toBe(200); @@ -156,7 +155,7 @@ describe("spaces e2e", () => { }); it("Bob can write his own message; Alice and Bob can both read", async () => { - const put = await call(app, "POST", "/xrpc/tools.atmo.space.putRecord", BOB, { + const put = await call(app, "POST", "/xrpc/test.spaces.space.putRecord", BOB, { spaceUri, collection: "app.event.message", record: { text: "see you there!" }, @@ -166,7 +165,7 @@ describe("spaces e2e", () => { const listAsAlice = await call( app, "GET", - `/xrpc/tools.atmo.space.listRecords?spaceUri=${encodeURIComponent(spaceUri)}&collection=app.event.message`, + `/xrpc/test.spaces.space.listRecords?spaceUri=${encodeURIComponent(spaceUri)}&collection=app.event.message`, ALICE ); expect(listAsAlice.status).toBe(200); @@ -186,11 +185,22 @@ describe("spaces e2e", () => { expect(true).toBe(true); }); + it("per-collection listRecords with ?spaceUri= requires auth", async () => { + // Public path (no spaceUri) works without auth; adding spaceUri forces the + // service-auth JWT path. With no valid JWT, 401. + const res = await app.fetch( + new Request( + `http://localhost/xrpc/${CONFIG.namespace}.location.listRecords?spaceUri=${encodeURIComponent(spaceUri)}` + ) + ); + expect([401, 501]).toContain(res.status); + }); + it("Charlie (not a member) cannot list messages", async () => { const res = await call( app, "GET", - `/xrpc/tools.atmo.space.listRecords?spaceUri=${encodeURIComponent(spaceUri)}&collection=app.event.message`, + `/xrpc/test.spaces.space.listRecords?spaceUri=${encodeURIComponent(spaceUri)}&collection=app.event.message`, CHARLIE ); expect(res.status).toBe(403); @@ -212,7 +222,7 @@ describe("spaces e2e", () => { // End-to-end works: createSpace, putRecord, listRecords const create = await splitApp.fetch( - new Request("http://localhost/xrpc/tools.atmo.space.admin.createSpace", { + new Request("http://localhost/xrpc/test.spaces.space.admin.createSpace", { method: "POST", headers: { "X-Test-Did": ALICE, "Content-Type": "application/json" }, body: JSON.stringify({ key: "split-test" }), @@ -222,7 +232,7 @@ describe("spaces e2e", () => { const { space } = await create.json() as any; const put = await splitApp.fetch( - new Request("http://localhost/xrpc/tools.atmo.space.putRecord", { + new Request("http://localhost/xrpc/test.spaces.space.putRecord", { method: "POST", headers: { "X-Test-Did": ALICE, "Content-Type": "application/json" }, body: JSON.stringify({ @@ -235,7 +245,7 @@ describe("spaces e2e", () => { const list = await splitApp.fetch( new Request( - `http://localhost/xrpc/tools.atmo.space.listRecords?spaceUri=${encodeURIComponent(space.uri)}&collection=app.event.location`, + `http://localhost/xrpc/test.spaces.space.listRecords?spaceUri=${encodeURIComponent(space.uri)}&collection=app.event.location`, { headers: { "X-Test-Did": ALICE } } ) ); @@ -247,7 +257,7 @@ describe("spaces e2e", () => { const listRes = await call( app, "GET", - `/xrpc/tools.atmo.space.listRecords?spaceUri=${encodeURIComponent(spaceUri)}&collection=app.event.location`, + `/xrpc/test.spaces.space.listRecords?spaceUri=${encodeURIComponent(spaceUri)}&collection=app.event.location`, BOB ); const list = await asJson(listRes); @@ -256,7 +266,7 @@ describe("spaces e2e", () => { const res = await call( app, "GET", - `/xrpc/tools.atmo.space.getRecord?spaceUri=${encodeURIComponent(spaceUri)}&collection=app.event.location&author=${ALICE}&rkey=${rkey}`, + `/xrpc/test.spaces.space.getRecord?spaceUri=${encodeURIComponent(spaceUri)}&collection=app.event.location&author=${ALICE}&rkey=${rkey}`, BOB ); expect(res.status).toBe(200); diff --git a/tests/spaces-invites.test.ts b/tests/spaces-invites.test.ts index d95138b..d3986ba 100644 --- a/tests/spaces-invites.test.ts +++ b/tests/spaces-invites.test.ts @@ -14,13 +14,12 @@ const CHARLIE = "did:plc:charlie"; const CONFIG: ContrailConfig = { namespace: "test.spaces", - collections: {}, + collections: { + message: { collection: "app.event.message" }, + }, spaces: { type: "tools.atmo.event.space", serviceDid: "did:web:test.example#svc", - defaultPolicies: { - "app.event.message": { read: "member", write: "member" }, - }, }, }; @@ -78,23 +77,23 @@ describe("invite e2e", () => { await initSchema(db, resolved); app = createApp(db, resolved, { spaces: { authMiddleware: fakeAuth() } }); - const res = await call(app, "POST", "/xrpc/tools.atmo.space.admin.createSpace", ALICE, { + const res = await call(app, "POST", "/xrpc/test.spaces.space.admin.createSpace", ALICE, { key: "party", }); spaceUri = ((await res.json()) as any).space.uri; }); it("non-owner cannot create an invite", async () => { - const res = await call(app, "POST", "/xrpc/tools.atmo.space.invite.create", BOB, { + const res = await call(app, "POST", "/xrpc/test.spaces.space.invite.create", BOB, { spaceUri, }); expect(res.status).toBe(403); }); it("owner creates an invite and Bob redeems it to become a member", async () => { - const create = await call(app, "POST", "/xrpc/tools.atmo.space.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.space.invite.create", ALICE, { spaceUri, - perms: "attendee", + perms: "write", }); expect(create.status).toBe(200); const { token, invite } = (await create.json()) as any; @@ -103,14 +102,14 @@ describe("invite e2e", () => { expect(invite.spaceUri).toBe(spaceUri); expect(invite.usedCount).toBe(0); - const redeem = await call(app, "POST", "/xrpc/tools.atmo.space.invite.redeem", BOB, { token }); + const redeem = await call(app, "POST", "/xrpc/test.spaces.space.invite.redeem", BOB, { token }); expect(redeem.status).toBe(200); const body = (await redeem.json()) as any; expect(body.spaceUri).toBe(spaceUri); - expect(body.perms).toBe("attendee"); + expect(body.perms).toBe("write"); // Bob is now a member — can write a message - const put = await call(app, "POST", "/xrpc/tools.atmo.space.putRecord", BOB, { + const put = await call(app, "POST", "/xrpc/test.spaces.space.putRecord", BOB, { spaceUri, collection: "app.event.message", record: { text: "yay" }, @@ -119,58 +118,58 @@ describe("invite e2e", () => { }); it("single-use invite rejects second redemption", async () => { - const create = await call(app, "POST", "/xrpc/tools.atmo.space.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.space.invite.create", ALICE, { spaceUri, maxUses: 1, }); const { token } = (await create.json()) as any; - const first = await call(app, "POST", "/xrpc/tools.atmo.space.invite.redeem", BOB, { token }); + const first = await call(app, "POST", "/xrpc/test.spaces.space.invite.redeem", BOB, { token }); expect(first.status).toBe(200); - const second = await call(app, "POST", "/xrpc/tools.atmo.space.invite.redeem", CHARLIE, { token }); + const second = await call(app, "POST", "/xrpc/test.spaces.space.invite.redeem", CHARLIE, { token }); expect(second.status).toBe(400); const body = (await second.json()) as any; expect(body.reason).toBe("expired-revoked-or-exhausted"); }); it("expired invite rejects redemption", async () => { - const create = await call(app, "POST", "/xrpc/tools.atmo.space.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.space.invite.create", ALICE, { spaceUri, expiresAt: Date.now() - 1000, }); const { token } = (await create.json()) as any; - const res = await call(app, "POST", "/xrpc/tools.atmo.space.invite.redeem", CHARLIE, { token }); + const res = await call(app, "POST", "/xrpc/test.spaces.space.invite.redeem", CHARLIE, { token }); expect(res.status).toBe(400); }); it("revoked invite rejects redemption and list filters it by default", async () => { - const create = await call(app, "POST", "/xrpc/tools.atmo.space.invite.create", ALICE, { + const create = await call(app, "POST", "/xrpc/test.spaces.space.invite.create", ALICE, { spaceUri, }); const { token, invite } = (await create.json()) as any; - const revoke = await call(app, "POST", "/xrpc/tools.atmo.space.invite.revoke", ALICE, { + const revoke = await call(app, "POST", "/xrpc/test.spaces.space.invite.revoke", ALICE, { spaceUri, tokenHash: invite.tokenHash, }); expect(revoke.status).toBe(200); expect(((await revoke.json()) as any).ok).toBe(true); - const tryRedeem = await call(app, "POST", "/xrpc/tools.atmo.space.invite.redeem", CHARLIE, { token }); + const tryRedeem = await call(app, "POST", "/xrpc/test.spaces.space.invite.redeem", CHARLIE, { token }); expect(tryRedeem.status).toBe(400); - const listActive = await call(app, "GET", `/xrpc/tools.atmo.space.invite.list?spaceUri=${encodeURIComponent(spaceUri)}`, ALICE); + const listActive = await call(app, "GET", `/xrpc/test.spaces.space.invite.list?spaceUri=${encodeURIComponent(spaceUri)}`, ALICE); const activeHashes = ((await listActive.json()) as any).invites.map((i: any) => i.tokenHash); expect(activeHashes).not.toContain(invite.tokenHash); - const listAll = await call(app, "GET", `/xrpc/tools.atmo.space.invite.list?spaceUri=${encodeURIComponent(spaceUri)}&includeRevoked=true`, ALICE); + const listAll = await call(app, "GET", `/xrpc/test.spaces.space.invite.list?spaceUri=${encodeURIComponent(spaceUri)}&includeRevoked=true`, ALICE); const allHashes = ((await listAll.json()) as any).invites.map((i: any) => i.tokenHash); expect(allHashes).toContain(invite.tokenHash); }); it("non-owner cannot list or revoke invites", async () => { - const listRes = await call(app, "GET", `/xrpc/tools.atmo.space.invite.list?spaceUri=${encodeURIComponent(spaceUri)}`, BOB); + const listRes = await call(app, "GET", `/xrpc/test.spaces.space.invite.list?spaceUri=${encodeURIComponent(spaceUri)}`, BOB); expect(listRes.status).toBe(403); - const revokeRes = await call(app, "POST", "/xrpc/tools.atmo.space.invite.revoke", BOB, { + const revokeRes = await call(app, "POST", "/xrpc/test.spaces.space.invite.revoke", BOB, { spaceUri, tokenHash: "nonexistent", }); expect(revokeRes.status).toBe(403); diff --git a/tests/types.test.ts b/tests/types.test.ts index bc71eb6..b18d746 100644 --- a/tests/types.test.ts +++ b/tests/types.test.ts @@ -5,9 +5,10 @@ import { getNestedValue, getRelationField, resolveConfig, - getCollectionNames, - getDiscoverableCollections, - getDependentCollections, + getCollectionShortNames, + getDiscoverableShortNames, + getDependentShortNames, + getCollectionNsids, } from "../src/core/types"; describe("validateFieldName", () => { @@ -39,13 +40,18 @@ describe("validateConfig", () => { it("passes for valid config", () => { expect(() => validateConfig({ + namespace: "test", collections: { - "test.collection": { + col: { + collection: "test.collection", queryable: { name: {}, startsAt: { type: "range" } }, relations: { - items: { collection: "test.item", field: "subject.uri", groupBy: "status" }, + items: { collection: "item", field: "subject.uri", groupBy: "status" }, }, }, + item: { + collection: "test.item", + }, }, }) ).not.toThrow(); @@ -54,8 +60,9 @@ describe("validateConfig", () => { it("rejects invalid queryable field names", () => { expect(() => validateConfig({ + namespace: "test", collections: { - "test.col": { queryable: { "bad field": {} } }, + col: { collection: "test.col", queryable: { "bad field": {} } }, }, }) ).toThrow("Invalid field name"); @@ -64,12 +71,15 @@ describe("validateConfig", () => { it("rejects invalid relation field", () => { expect(() => validateConfig({ + namespace: "test", collections: { - "test.col": { + col: { + collection: "test.col", relations: { - r: { collection: "test.other", field: "bad field" }, + r: { collection: "other", field: "bad field" }, }, }, + other: { collection: "test.other" }, }, }) ).toThrow("Invalid field name"); @@ -78,16 +88,35 @@ describe("validateConfig", () => { it("rejects invalid groupBy", () => { expect(() => validateConfig({ + namespace: "test", collections: { - "test.col": { + col: { + collection: "test.col", relations: { - r: { collection: "test.other", groupBy: "bad;field" }, + r: { collection: "other", groupBy: "bad;field" }, }, }, + other: { collection: "test.other" }, }, }) ).toThrow("Invalid field name"); }); + + it("rejects relation to unknown collection short name", () => { + expect(() => + validateConfig({ + namespace: "test", + collections: { + col: { + collection: "test.col", + relations: { + r: { collection: "missing" }, + }, + }, + }, + }) + ).toThrow("references unknown collection"); + }); }); describe("getNestedValue", () => { @@ -121,61 +150,90 @@ describe("getRelationField", () => { }); describe("resolveConfig", () => { - it("adds default profile collection", () => { - const resolved = resolveConfig({ collections: { "test.col": {} } }); - expect(resolved.collections["app.bsky.actor.profile"]).toEqual({ discover: false }); + it("adds default profile collection (keyed by short name `profile`)", () => { + const resolved = resolveConfig({ + namespace: "test", + collections: { col: { collection: "test.col" } }, + }); + expect(resolved.collections["profile"]).toBeDefined(); + expect(resolved.collections["profile"].collection).toBe("app.bsky.actor.profile"); + expect(resolved.collections["profile"].discover).toBe(false); }); - it("does not overwrite existing profile collection config", () => { + it("does not overwrite existing profile entry", () => { const resolved = resolveConfig({ - collections: { "app.bsky.actor.profile": { queryable: { displayName: {} } } }, + namespace: "test", + collections: { + profile: { collection: "app.bsky.actor.profile", queryable: { displayName: {} } }, + }, }); - expect(resolved.collections["app.bsky.actor.profile"].queryable).toEqual({ displayName: {} }); + expect(resolved.collections["profile"].queryable).toEqual({ displayName: {} }); }); it("uses custom profiles", () => { const resolved = resolveConfig({ + namespace: "test", collections: {}, profiles: ["custom.profile"], }); - expect(resolved.profiles).toEqual([{ collection: "custom.profile" }]); - expect(resolved.collections["custom.profile"]).toEqual({ discover: false }); - expect(resolved.collections["app.bsky.actor.profile"]).toBeUndefined(); + expect(resolved.profiles?.[0].collection).toBe("custom.profile"); + expect(resolved.collections["profile"]).toBeDefined(); + expect(resolved.collections["profile"].collection).toBe("custom.profile"); }); it("applies default jetstreams and relays", () => { - const resolved = resolveConfig({ collections: {} }); + const resolved = resolveConfig({ namespace: "test", collections: {} }); expect(resolved.jetstreams).toHaveLength(4); expect(resolved.relays).toHaveLength(1); }); + + it("builds nsidToShort reverse map", () => { + const resolved = resolveConfig({ + namespace: "test", + collections: { + event: { collection: "test.event" }, + rsvp: { collection: "test.rsvp" }, + }, + }); + expect(resolved._resolved.nsidToShort["test.event"]).toBe("event"); + expect(resolved._resolved.nsidToShort["test.rsvp"]).toBe("rsvp"); + }); }); -describe("getCollectionNames / getDiscoverableCollections / getDependentCollections", () => { +describe("collection lookup helpers", () => { const config = resolveConfig({ + namespace: "test", collections: { - "test.main": {}, - "test.dep": { discover: false }, + main: { collection: "test.main" }, + dep: { collection: "test.dep", discover: false }, }, }); - it("getCollectionNames returns all collections", () => { - const names = getCollectionNames(config); - expect(names).toContain("test.main"); - expect(names).toContain("test.dep"); - expect(names).toContain("app.bsky.actor.profile"); + it("getCollectionShortNames returns all short names (including auto-added profile)", () => { + const names = getCollectionShortNames(config); + expect(names).toContain("main"); + expect(names).toContain("dep"); + expect(names).toContain("profile"); + }); + + it("getCollectionNsids returns all record NSIDs", () => { + const nsids = getCollectionNsids(config); + expect(nsids).toContain("test.main"); + expect(nsids).toContain("test.dep"); + expect(nsids).toContain("app.bsky.actor.profile"); }); - it("getDiscoverableCollections excludes discover:false", () => { - const discoverable = getDiscoverableCollections(config); - expect(discoverable).toContain("test.main"); - expect(discoverable).not.toContain("test.dep"); - expect(discoverable).not.toContain("app.bsky.actor.profile"); + it("getDiscoverableShortNames excludes discover:false", () => { + const discoverable = getDiscoverableShortNames(config); + expect(discoverable).toContain("main"); + expect(discoverable).not.toContain("dep"); + expect(discoverable).not.toContain("profile"); }); - it("getDependentCollections returns discover:false", () => { - const dependent = getDependentCollections(config); - expect(dependent).toContain("test.dep"); - expect(dependent).toContain("app.bsky.actor.profile"); - expect(dependent).not.toContain("test.main"); + it("getDependentShortNames returns discover:false", () => { + const dependent = getDependentShortNames(config); + expect(dependent).toContain("dep"); + expect(dependent).toContain("profile"); + expect(dependent).not.toContain("main"); }); }); diff --git a/tsup.config.ts b/tsup.config.ts index d78905c..cffcd4a 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ "src/index.ts", "src/server.ts", "src/generate.ts", - "src/lexicons.ts", + "src/publish.ts", "src/adapters/sqlite.ts", "src/adapters/postgres.ts", ], -- 2.51.2