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 + stylesapp.js— all logic (ES module)vendor/atproto.js— self-contained bundle of@atproto/oauth-client-browser,@atproto/api,marked, anddompurify, served same-originvendor/build.sh— regeneratesvendor/atproto.jsclient-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) #
- Set your origin.
client-metadata.jsonis already pointed athttps://atmob.in. If you deploy somewhere else, replace everyatmob.inin 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>/"]
- Serve the
web/directory at the root of that host over HTTPS, so thatclient-metadata.jsonresolves athttps://<host>/client-metadata.jsonand the app loads athttps://<host>/. - Done.
app.jsfetches./client-metadata.jsonrelative 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_RESOLVERinapp.jsishttps://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.jsonfordid:web) to find each author's PDS, then hits their publicgetRecord— no login required to view. - Markdown renders; code isn't syntax-highlighted. Bundle descriptions,
comments, and
.mdfiles render as sanitized markdown (viamarked+dompurify). Non-markdown code still shows in a plain<pre>; to add highlighting, run a highlighter over the<code>blocks infileBlock()and add it tovendor/build.shso 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 anat://URI (?uri=...).