Skip to content

Latest commit

 

History

History
155 lines (115 loc) · 7.51 KB

File metadata and controls

155 lines (115 loc) · 7.51 KB

atproto integration

edgeBox is an atproto AppView. Canonical user data lives in each user's PDS as signed records; our server reads via the AppView cache with live-fetch fallback, writes on behalf of the user using their OAuth-ish session, and hydrates Bluesky posts through Bluesky's public AppView for counts and inline quotes.

The migration to AT Protocol shipped across several phases; test-harness migration and final legacy cleanup are in progress.

Directory map

lexicons/com/edgebox/          # canonical schemas we authored
  profile.json                 # edgeBox-specific profile extensions (links, accent)
  box/layout.json              # grid layout spec record
  actor/manifest.json          # installed actors (viewer-owned)
  actor/publication.json       # actor published by an author
  matter/{post,track,image,game}.json
  graph/follow.json            # edgeBox-specific follow (complements app.bsky.graph.follow)
  agent/permissionSet.json     # external-agent grant

server/atproto/
  client.js        # AtpAgent + handle→DID→PDS resolver + XRPC helpers
  bsky.js          # Bluesky AppView hydration (getPosts, listReposts)
  jetstream.js     # firehose consumer (disabled on prod; needs Node 22+ WebSocket)
  indexer.js       # wires jetstream → appview cache
  lexicons.js      # loads + validates lexicons/ on disk
  writes.js        # putRecordForSession / deleteRecordForSession

server/appview/
  cache.js         # DID-keyed record + profile-view cache (firehose-populated)
  boxes.js         # compiled Box binary cache (data/appview/boxes/{did}/box.wasm)
  compile_queue.js # debounced per-DID compile queue
  feed.js          # builds feed from cache (when populated)
  live.js          # live XRPC fallback for feed + profile (no firehose needed)
  normalize.js     # translates atproto records → edgeBox Post schema
  layout.js, matter.js, marketplace.js, permissions.js

server/oauth.js    # handle+app-password login against user's PDS (Phase 13G → DPoP)
server/auth.js     # file-backed DID-keyed sessions

Data paths:

  • data/appview/sessions/{uuid}.json — active sessions (DID, handle, accessJwt, refreshJwt, pdsUrl)
  • data/appview/index/records/{collection}/{did-encoded}/{rkey}.json — cached records
  • data/appview/index/profiles/{did-encoded}.json — merged profile view (Bluesky + edgeBox profile)
  • data/appview/index/handles.json — handle → DID mapping
  • data/appview/boxes/{did-encoded}/box.wasm — compiled Box binary (derived artifact, AppView-owned)

Host API v1 — additive extensions

All new fields are #[serde(default)] — old Boxes ignore them, new Boxes use them. Never remove or change field types.

Profile gains bannerUrl: Option<String> — URL pointing at author's PDS sync.getBlob for the Bluesky banner.

Post gains:

Field Type Source
facets Vec<Facet> app.bsky.feed.post.facets (byte spans for link/mention/tag)
embeds Vec<Embed> app.bsky.embed.{images,external,record,recordWithMedia}
author Option<AuthorRef> profile view of the post's author (handle, displayName, avatarUrl)
uri, cid Option<String> for interaction writes (like/repost/reply target)
likeCount, repostCount, replyCount u32 Bluesky AppView hydration
viewerLikeUri, viewerRepostUri Option<String> existing viewer interaction record URIs (for unlike/unrepost)
repostedBy Option<AuthorRef> set when this feed item is a repost — the reposter

Embed gains for quote types: quotedText, quotedAuthor — populated by hydration.

See boxes/sdk/box_sdk.rs for the canonical Rust shapes.

Rich rendering

boxes/sdk/rich.rs provides reusable helpers that every View can drop in:

  • render_post_content(content, facets) — interleaves plain text + <a> spans for facets. XSS-safe.
  • render_embed(embed) — renders image figures, external link cards, quote blocks (hydrated or link-only).
  • render_interaction_bar(post) — three-button bar (reply/repost/like) with counts and viewer state.
  • render_repost_banner(post) — "X reposted" header when reposted_by is set.
  • _raw variants of the interaction helpers take primitives — used by FeedBox (which has its own post struct).

The emitted HTML uses .lb-* classes; CSS lives in server/index.js::buildPage so it cascades into any Wasm-rendered page.

Server endpoints

Identity + profile

Method Path Purpose
GET /oauth/client-metadata.json OAuth client metadata (Phase 13G→DPoP stub)
POST /auth/login handle + app-password → session
POST /auth/logout destroy session
GET /@handle/manifest public capability manifest

Writes against the user's PDS

All require session; each wraps server/atproto/writes.js::putRecordForSession with the relevant collection.

Method Path Writes
POST /@handle/edit com.edgebox.box.layout (self) → triggers compile
POST /api/matter com.edgebox.matter.{post,track,image,game} (by type)
POST /api/subscribe / /api/unsubscribe com.edgebox.graph.follow create/delete
POST /api/interact/like app.bsky.feed.like
POST /api/interact/unlike delete by rkey
POST /api/interact/repost app.bsky.feed.repost
POST /api/interact/unrepost delete by rkey
POST /api/interact/reply app.bsky.feed.post with reply field
POST /@handle/agent/invoke agent skill invocation (X-Agent-Did header + permissionSet grant, or session)

Live fetch vs cache

The AppView cache is populated by Jetstream. Since Jetstream needs Node 22's native WebSocket (not available on prod's Node 20), production uses the live-fetch path (server/appview/live.js):

  • liveProfilePostsForDid — for /@handle
  • liveFeedForViewer — for /feed

Both:

  1. List raw records (matter + app.bsky.feed.post + app.bsky.feed.repost)
  2. Batch all Bluesky post URIs + repost subjects
  3. Single hydration call to api.bsky.app/xrpc/app.bsky.feed.getPosts
  4. Merge hydrated counts + inline quotes back into the normalized posts
  5. Emit reposts as separate feed rows with a repostedBy ref

Enable Jetstream caching later with JETSTREAM=1 on Node 22.

Tests

Tier 1 (unit) runs on node --test:

npm run test:unit

Helpers:

  • tests/helpers/tempdir.jswithTempDataDir(fn) — isolates DATA_DIR per test
  • tests/helpers/fetch_stub.jswithFetchStub(routes, fn) — intercepts globalThis.fetch

Coverage: lexicons, cache, client (handle/DID/XRPC), jetstream dispatch, indexer→cache, auth sessions, oauth, PDS writes, appview modules, normalizer, Bluesky hydration.

146 unit tests run in ~500ms.

Adding a new atproto-aware View

  1. Add FRAGMENTS.<type>(node) in scripts/generate_box.js — pure codegen returning Rust
  2. Extend VALID_TYPES with the new type
  3. Update boxes/generated/box_v1/src/lib.rs if this View should appear by default
  4. Add picker entry + property panel in boxes/dslbox/src/lib.rs (under its category)
  5. Rebuild: bash scripts/build_box.sh boxes/dslbox data/boxes/_dslbox.wasm; rebuild default if changed

Matter-type Views additionally need the relevant live fetcher in server/appview/live.js to populate the input JSON.

Deploy notes

  • First deploy after a pull needs npm install (dependencies exist now)
  • Restart the systemd service after deploying.
  • Existing sessions reset on restart (file-backed); users re-log-in once
  • Jetstream stays off unless JETSTREAM=1 is set and Node is 22+
  • INVITE_CODE env var is a no-op now (atproto PDS is the account source)