A pastebin clone for atproto
README.md

atmob paste — static frontend #

A no-build single-page app for the in.atmob.paste.* lexicons. Sign in with atproto OAuth, publish multi-file pastes/gists, and view any public bundle by its at:// URI.

Files #

  • index.html — markup + styles
  • app.js — all logic (ES module)
  • vendor/atproto.js — self-contained bundle of @atproto/oauth-client-browser, @atproto/api, marked, and dompurify, served same-origin
  • vendor/build.sh — regenerates vendor/atproto.js
  • client-metadata.json — your OAuth client identity (served publicly)

Dependencies are vendored, not loaded from a CDN #

wisp.place blocks cross-origin module scripts (and esm.sh has been unreliable), so the atproto libraries are bundled into vendor/atproto.js and served from your own origin. To update them:

cd web/vendor
./build.sh   # needs node + npm; rewrites atproto.js

Everything else in the app is same-origin; the only cross-origin traffic at runtime is the OAuth/XRPC calls to PDS hosts and plc.directory, which wisp.place does not restrict (no connect-src CSP).

Deploy (wisp.place / any static host) #

  1. Set your origin. client-metadata.json is already pointed at https://atmob.in. If you deploy somewhere else, replace every atmob.in in that file with your real host. The three URLs must match your live site:
    • client_id → https://<host>/client-metadata.json (this exact URL is your client id; the auth server fetches it)
    • client_uri → https://<host>
    • redirect_uris → ["https://<host>/"]
  2. Serve the web/ directory at the root of that host over HTTPS, so that client-metadata.json resolves at https://<host>/client-metadata.json and the app loads at https://<host>/.
  3. Done. app.js fetches ./client-metadata.json relative to wherever it's served, so no code changes are needed as long as step 1 matches your host.

The redirect_uris is the site root /; the OAuth callback lands back on the same page and client.init() completes the flow automatically.

Local development #

Loopback origins get a built-in dev client from the PDS — no public metadata needed. Serve over HTTP on 127.0.0.1 (not localhost, per the atproto OAuth loopback rules; the library will redirect localhost → 127.0.0.1 anyway):

cd web
python3 -m http.server 8080
# open http://127.0.0.1:8080

In loopback mode the app requests the transition:generic scope (the granular repo:* scopes require the full client metadata, which loopback can't supply).

Scopes requested (production) #

atproto
repo:in.atmob.paste.bundle
repo:in.atmob.paste.document
repo:in.atmob.paste.blob
repo:in.atmob.paste.star
repo:in.atmob.paste.comment
blob:*/*

Granular scopes mean the consent screen says "write atmob paste records and upload files" — not "access to everything." If a target PDS is too old to support granular scopes, swap the whole scope string for atproto transition:generic.

Known caveats / next steps #

  • Handle-resolution privacy. HANDLE_RESOLVER in app.js is https://bsky.social, which leaks the sign-in handle + IP to Bluesky. If you self-host a PDS, point it there instead.
  • Reading others' repos uses plc.directory (and .well-known/did.json for did:web) to find each author's PDS, then hits their public getRecord — no login required to view.
  • Markdown renders; code isn't syntax-highlighted. Bundle descriptions, comments, and .md files render as sanitized markdown (via marked + dompurify). Non-markdown code still shows in a plain <pre>; to add highlighting, run a highlighter over the <code> blocks in fileBlock() and add it to vendor/build.sh so it stays same-origin.
  • No app-view / aggregation. "Your pastes" lists your own repo directly. Star counts, global feeds, and search need a separate indexer (e.g. a Jetstream consumer or Constellation backlinks) — not part of this static app.
  • resolveHandleToDid() is included for a future "view by @handle" box; the viewer currently takes an at:// URI (?uri=...).