diff --git a/apps/group-chat/src/lib/contrail/client.ts b/apps/group-chat/src/lib/contrail/client.ts index 2b86128..13f8f51 100644 --- a/apps/group-chat/src/lib/contrail/client.ts +++ b/apps/group-chat/src/lib/contrail/client.ts @@ -8,7 +8,7 @@ export interface Profile { /** * Extract a simple profile from a contrail profile entry. - * Contrail returns { did, handle, record: { displayName, avatar, ... } } + * Contrail returns { did, handle, value: { displayName, avatar, ... } } * while components expect { handle, displayName?, avatar? }. */ export function extractProfile(entry: { diff --git a/apps/group-chat/src/lib/rooms/profiles.svelte.ts b/apps/group-chat/src/lib/rooms/profiles.svelte.ts index a0d0a39..c2999ab 100644 --- a/apps/group-chat/src/lib/rooms/profiles.svelte.ts +++ b/apps/group-chat/src/lib/rooms/profiles.svelte.ts @@ -35,7 +35,7 @@ export function ensureProfile(did: string): void { profiles?: Array<{ did: string; handle?: string | null; - record?: { displayName?: string; avatar?: string }; + value?: { displayName?: string; avatar?: string }; }>; }; const entry = data.profiles?.[0]; diff --git a/apps/group-chat/src/lib/rooms/watch.svelte.ts b/apps/group-chat/src/lib/rooms/watch.svelte.ts index 2d57f6a..6d54c5f 100644 --- a/apps/group-chat/src/lib/rooms/watch.svelte.ts +++ b/apps/group-chat/src/lib/rooms/watch.svelte.ts @@ -59,7 +59,7 @@ export type RecordShapeOf = K extends keyof Records : Record : Record; -/** WatchRecord with a typed `record` payload. Mirrors WatchRecord's explicit +/** WatchRecord with a typed `value` payload. Mirrors WatchRecord's explicit * fields but without the `[k: string]: unknown` catchall (which would * poison property access under `Omit`). */ export interface TypedWatchRecord { @@ -67,7 +67,7 @@ export interface TypedWatchRecord { did: string; rkey: string; collection: string; - record: R; + value: R; time_us?: number; indexed_at?: number; cid?: string | null; diff --git a/apps/group-chat/src/routes/+layout.server.ts b/apps/group-chat/src/routes/+layout.server.ts index 02bc114..5e32dc9 100644 --- a/apps/group-chat/src/routes/+layout.server.ts +++ b/apps/group-chat/src/routes/+layout.server.ts @@ -23,7 +23,7 @@ export const load: LayoutServerLoad = async ({ locals, platform }) => { profiles?: Array<{ did: string; handle?: string | null; - record?: { displayName?: string; avatar?: string }; + value?: { displayName?: string; avatar?: string }; }>; }; const entry = data.profiles?.[0]; diff --git a/apps/group-chat/src/routes/+page.server.ts b/apps/group-chat/src/routes/+page.server.ts index 3b2f8e5..1ddb58c 100644 --- a/apps/group-chat/src/routes/+page.server.ts +++ b/apps/group-chat/src/routes/+page.server.ts @@ -28,7 +28,7 @@ export const load: PageServerLoad = async ({ locals, platform }) => { records: Array<{ did: string; rkey: string; - record: { + value: { communityDid?: string; name?: string; description?: string; diff --git a/apps/group-chat/src/routes/c/[communityDid]/+layout.server.ts b/apps/group-chat/src/routes/c/[communityDid]/+layout.server.ts index 2b7ee00..50435da 100644 --- a/apps/group-chat/src/routes/c/[communityDid]/+layout.server.ts +++ b/apps/group-chat/src/routes/c/[communityDid]/+layout.server.ts @@ -25,7 +25,7 @@ export const load: LayoutServerLoad = async ({ locals, params, platform }) => { records: Array<{ did: string; rkey: string; - record: { + value: { communityDid?: string; name?: string; description?: string; diff --git a/apps/sveltekit-cloudflare-workers/src/lib/contrail/client.ts b/apps/sveltekit-cloudflare-workers/src/lib/contrail/client.ts index 2b86128..13f8f51 100644 --- a/apps/sveltekit-cloudflare-workers/src/lib/contrail/client.ts +++ b/apps/sveltekit-cloudflare-workers/src/lib/contrail/client.ts @@ -8,7 +8,7 @@ export interface Profile { /** * Extract a simple profile from a contrail profile entry. - * Contrail returns { did, handle, record: { displayName, avatar, ... } } + * Contrail returns { did, handle, value: { displayName, avatar, ... } } * while components expect { handle, displayName?, avatar? }. */ export function extractProfile(entry: { diff --git a/docs/02-querying.md b/docs/02-querying.md index f2cec15..72720b8 100644 --- a/docs/02-querying.md +++ b/docs/02-querying.md @@ -7,7 +7,7 @@ Once [indexing](./01-indexing.md) is set up, every collection you declared gets | `{namespace}.{short}.listRecords` | Paginated list with filters, sorts, hydration | | `{namespace}.{short}.getRecord?uri=…` | Single record by AT-URI | -Plus a few top-level ones: `{namespace}.getProfile`, `{namespace}.getCursor`, `{namespace}.getOverview`, `{namespace}.notifyOfUpdate`, `{namespace}.permissionSet`. +Plus a few top-level ones: `{namespace}.getProfile`, `{namespace}.getCursor`, `{namespace}.getOverview`, `{namespace}.notifyOfUpdate`, `{namespace}.permissionSet`, `{namespace}.lexicons`. ## HTTP (what most callers use) @@ -73,13 +73,15 @@ Each record response is a flat shape: { "uri": "at://did:plc:.../community.lexicon.calendar.event/...", "cid": "...", - "record": { "name": "Rust meetup", "startsAt": "2026-03-16T...", ... }, + "value": { "name": "Rust meetup", "startsAt": "2026-03-16T...", ... }, "rsvpsCount": 42, // from relations "rsvpsGoingCount": 30, // relations + references appear here only when hydrated } ``` +The `value` field carries the record body — same shape as atproto's `com.atproto.repo.listRecords#record`. `did`, `collection`, `rkey`, and `time_us` are also returned alongside as optional extras. + ### `?hydrateRel=N` (relations) Embeds the latest N child records per group, inline under the parent: @@ -94,11 +96,11 @@ Returns: { "records": [{ "uri": "at://.../event/...", - "record": { "name": "..." }, + "value": { "name": "..." }, "rsvpsCount": 42, "rsvps": { - "going": [ {uri, record}, ... 5 items ], - "interested":[ {uri, record}, ... 5 items ] + "going": [ {uri, cid, value}, ... 5 items ], + "interested":[ {uri, cid, value}, ... 5 items ] } }] } @@ -114,7 +116,7 @@ Embeds the single referenced parent record — useful for RSVP lists that need t /xrpc/com.example.rsvp.listRecords?subjectUri=at://.../event/...&hydrateEvent=true ``` -Each RSVP record in the response gains an `event: {uri, cid, record}` field. +Each RSVP record in the response gains an `event: {uri, cid, value}` field. ### `?profiles=true` @@ -124,18 +126,28 @@ Opt in to profile + handle hydration for every DID referenced in the result: /xrpc/com.example.event.listRecords?profiles=true ``` -Response grows a top-level `profiles` map keyed by DID: +Response grows a top-level `profiles` array, one entry per (DID, configured profile NSID): ```jsonc { "records": [...], - "profiles": { - "did:plc:alice...": { "handle": "alice.bsky.social", "record": {...} } - } + "profiles": [ + { + "did": "did:plc:alice...", + "handle": "alice.bsky.social", + "uri": "at://did:plc:alice.../app.bsky.actor.profile/self", + "cid": "...", + "collection": "app.bsky.actor.profile", + "rkey": "self", + "value": { /* profile record body */ } + } + ] } ``` -Which profile NSID to hydrate from is configured at the top level of Contrail's config (`profiles`, defaults to `["app.bsky.actor.profile"]`). +A DID with no profile record (or whose handle resolved but profile didn't) shows up as a bare `{ did, handle }` entry — `uri`/`cid`/`value` are omitted. With multiple profile NSIDs configured, you'll see one entry per (DID × NSID) that resolved. + +Which profile NSID(s) to hydrate from is configured at the top level of Contrail's config (`profiles`, defaults to `["app.bsky.actor.profile"]`). ## Full-text search diff --git a/docs/04-auth.md b/docs/04-auth.md index 4f01922..dedd137 100644 --- a/docs/04-auth.md +++ b/docs/04-auth.md @@ -77,14 +77,12 @@ Tokens can be revoked (`invite.revoke`), expire automatically (`ttl`), and be ex Realtime subscriptions (`watchRecords`) can't use regular service-auth JWTs for two reasons: the WebSocket upgrade can't carry arbitrary headers, and an open socket would outlive a 60s JWT TTL. So contrail uses separate short-lived tickets. -Server-side minting: +Server-side minting comes in two flavours: -``` -com.example.realtime.ticket { spaceUri } - → { ticket: "...", expiresAt: 1234567890 } -``` +- `com.example.realtime.ticket` — POST `{ topic }` (e.g. `"space:at://..."`) → `{ ticket, topics, expiresAt }`. Bare topic-list ticket, used with the generic `.realtime.subscribe` endpoint. +- `.watchRecords?mode=ws&spaceUri=…` (or `&actor=…`) handshake — returns `{ snapshot, ticket, wsUrl, sinceTs, ticketTtlMs, querySpec }`. The ticket is bound to `(did, topics, querySpec)` and is the one to use for the per-collection `watchRecords` stream — both for SSE (`?ticket=…`) and the subsequent WS upgrade. -The ticket is signed by `realtime.ticketSecret` (a 32-byte random, configured once), bound to `(did, spaceUri, querySpec)`. Client uses it on the SSE or WS handshake via `?ticket=...`. +Both flavours are signed by `realtime.ticketSecret` (a 32-byte random, configured once). Clients hand the ticket off via `?ticket=...` on connect. In the `@atmo-dev/contrail-sync` client: diff --git a/docs/05-spaces.md b/docs/05-spaces.md index 2e6235d..2862c42 100644 --- a/docs/05-spaces.md +++ b/docs/05-spaces.md @@ -51,7 +51,7 @@ Space-specific wiring: | `?spaceUri=…` + JWT | one space (ACL-gated) | | JWT, no `spaceUri` | public **unioned** with every space the caller is a member of | -Filters, sorts, hydration, and references work across all three. Records from a space carry a `space: ` field. +Filters, sorts, hydration, and references work across all three. Records from a space carry a `space: ` field on `listRecords`/`getRecord` responses (the watchRecords stream uses `_space` instead — see [Sync](./07-sync.md)). ## Invites diff --git a/docs/07-sync.md b/docs/07-sync.md index d0419dc..2a7691b 100644 --- a/docs/07-sync.md +++ b/docs/07-sync.md @@ -42,7 +42,7 @@ Tickets are minted server-side via `com.example.realtime.ticket` (or any app-spe ## Optimistic updates ```ts -store.addOptimistic({ rkey, did, record: { text: "hi" } }); +store.addOptimistic({ rkey, did, value: { text: "hi" } }); // later, on mutation failure: store.markFailed(rkey, err); // or explicit rollback: diff --git a/todo/other-stuff.md b/todo/other-stuff.md new file mode 100644 index 0000000..aee9482 --- /dev/null +++ b/todo/other-stuff.md @@ -0,0 +1 @@ +- allow running custom functions before ingestion (e.g. for filtering out unallowed writes) \ No newline at end of file