Summary
Finishes the two remaining publishing items from #5.
Rich-text facets
- Links,
#hashtags, and@mentionsare detected in post text and attached as ATProto facets with UTF-8 byte ranges (not char offsets — emoji-safe, covered by tests) - Mentions resolve handle → DID via
com.atproto.identity.resolveHandle; unresolvable mentions degrade gracefully to plain text - Trailing punctuation is trimmed from links; numeric-only tags and tags inside URLs are skipped
- Callers can pass precomputed
facetsto skip auto-detection
Image upload
- Composer: attach button + filename chip (image types, 1MB cap client- and driver-side)
- The file flows as multipart through
CrosspostPublishAction→PublishContent.medianow carriesbytes/mimeTypealongsideurl BlueskyServicefetches URL-only media server-side; the driver uploads viacom.atproto.repo.uploadBloband embedsapp.bsky.embed.images(up to 4)- Images take precedence over a link card in the single-embed slot — the link stays clickable via its facet
- Instagram's existing URL-based flow is untouched
Verification
bun test— 13 pass (8 new: facet offsets/trimming/skipping, mention resolution + fallback, blob upload + embed, size cap)bunx --bun pickier .clean,bun run typecheckexit 0- Live multipart publish exercised against the dev API (fails only at missing credentials, confirming the file reaches the driver path); attach/clear UI verified in-browser
Refs #5