diff --git a/.githooks/post-commit b/.githooks/post-commit --- a/.githooks/post-commit +++ b/.githooks/post-commit @@ -28,4 +28,11 @@ echo "📺 Landing page changed — deploying to silo..." fish "$REPO_ROOT/feed/deploy-silo.fish" --landing 2>&1 | tail -3 & fi +# Auto-deploy AT PDS frontend when tracked files change +AT_FRONTEND_FILES="at/index.html|at/user-page.html|at/media-modal.js|at/media-records.js" +if git diff-tree --no-commit-id --name-only -r HEAD | grep -qE "^($AT_FRONTEND_FILES)$"; then + echo "🔮 AT frontend changed — deploying to PDS..." + bash "$REPO_ROOT/at/scripts/deploy-at-frontend.sh" 2>&1 | tail -3 & +fi + exit 0 diff --git a/at/STANDARD-SITE-SYNC.md b/at/STANDARD-SITE-SYNC.md new file mode 100644 --- /dev/null +++ b/at/STANDARD-SITE-SYNC.md @@ -0,0 +1,51 @@ +# Standard.site Mirror Sync + +Shallow-copy selected `computer.aesthetic.*` ATProto records into `site.standard.document`. + +## Recommended First-Wave Sources + +- `computer.aesthetic.paper` → high-fidelity long-form records (`papers.aesthetic.computer`) +- `computer.aesthetic.news` → headline/body documents (`news.aesthetic.computer`) +- `computer.aesthetic.piece` → canonical piece pages (`aesthetic.computer/`) + +These are the default sources in the script. + +## Script + +`at/scripts/atproto/backfill-standard-site-documents.mjs` + +This script: + +- Logs into each user repo with existing ATProto credentials +- Reads selected source records +- Creates `site.standard.document` records with mapped fields +- Deduplicates by `sourceAtUri` (stored on target records) +- Never modifies source records (shallow copy) + +## Usage + +```bash +# Dry run (recommended first) +npm run at:standard:dry + +# Live sync (default sources: paper,news,piece) +npm run at:standard:sync + +# Single user +node at/scripts/atproto/backfill-standard-site-documents.mjs --dry-run --user @jeffrey + +# Custom sources +node at/scripts/atproto/backfill-standard-site-documents.mjs --dry-run --sources=paper,news,piece,kidlisp + +# Limit records per source per user +node at/scripts/atproto/backfill-standard-site-documents.mjs --dry-run --limit 25 + +# Limit number of users processed (staged rollout) +node at/scripts/atproto/backfill-standard-site-documents.mjs --dry-run --user-limit 10 +``` + +## Notes + +- Target collection: `site.standard.document` +- Required target fields are always populated: `site`, `title`, `publishedAt` +- Optional mappings include `path`, `description`, `textContent`, and `tags` diff --git a/at/cli.mjs b/at/cli.mjs --- a/at/cli.mjs +++ b/at/cli.mjs @@ -490,6 +490,26 @@ console.log(); } +async function commandSyncStandard() { + const { execFileSync } = await import("child_process"); + const { fileURLToPath } = await import("url"); + + const scriptUrl = new URL( + "./scripts/atproto/backfill-standard-site-documents.mjs", + import.meta.url, + ); + const scriptPath = fileURLToPath(scriptUrl); + const passthroughArgs = process.argv.slice(3); + + try { + execFileSync("node", [scriptPath, ...passthroughArgs], { + stdio: "inherit", + }); + } catch (error) { + process.exitCode = error.status || 1; + } +} + async function commandSSH(args) { const { execSync } = await import("child_process"); const ip = process.env.PDS_SSH_HOST || "165.227.120.137"; @@ -563,6 +583,7 @@ invite Generate PDS invite code accounts [--limit=N] List PDS accounts account:check Inspect account & record counts sync:status Record counts across collections + sync:standard [options] Mirror AC records to site.standard.document Server: ssh [command] SSH into PDS droplet (or run command) @@ -585,6 +606,7 @@ ac-at post "Hello from AC!" --image=painting.png ac-at invite ac-at account:check jeffrey.at.aesthetic.computer ac-at sync:status + ac-at sync:standard --dry-run --sources=paper,news,piece --limit=25 `); } @@ -604,6 +626,7 @@ invite: commandInvite, accounts: commandAccounts, "account:check": commandAccountCheck, "sync:status": commandSyncStatus, + "sync:standard": commandSyncStandard, ssh: commandSSH, "env:set": commandEnvSet, }; diff --git a/at/scripts/atproto/backfill-standard-site-documents.mjs b/at/scripts/atproto/backfill-standard-site-documents.mjs new file mode 100644 --- /dev/null +++ b/at/scripts/atproto/backfill-standard-site-documents.mjs @@ -0,0 +1,526 @@ +#!/usr/bin/env node + +/** + * Backfill Standard.site Documents + * + * Shallow-copies existing Aesthetic Computer ATProto records into + * `site.standard.document` records. + * + * Default source collections: + * - computer.aesthetic.paper + * - computer.aesthetic.news + * - computer.aesthetic.piece + * + * Usage: + * node at/scripts/atproto/backfill-standard-site-documents.mjs [options] + * + * Options: + * --dry-run Show what would be synced without creating records + * --user @handle Only process a single user's repo + * --user-limit N Only process first N users (for testing/staged rollout) + * --sources list Comma-separated: paper,news,piece,kidlisp,mood + * --limit N Max source records per source collection (per user) + * --batch-size N Pause every N creates (default: 20) + * --delay MS Pause length in milliseconds (default: 300) + */ + +import { AtpAgent } from "@atproto/api"; +import { connect } from "../../../system/backend/database.mjs"; +import { config } from "dotenv"; + +config({ path: "../../../system/.env" }); + +const PDS_URL = process.env.PDS_URL || "https://at.aesthetic.computer"; +const TARGET_COLLECTION = "site.standard.document"; +const MAX_PAGE_SIZE = 100; + +const SOURCE_CONFIG = { + paper: { + collection: "computer.aesthetic.paper", + toDocument(sourceRecord) { + const value = sourceRecord.value || {}; + const rkey = rkeyFromUri(sourceRecord.uri); + const slugRaw = String(value.slug || value.ref || rkey || "").trim(); + const slug = slugRaw.replace(/^\/+/, ""); + const path = slug + ? slug.endsWith(".pdf") + ? `/${slug}` + : `/${slug}.pdf` + : `/paper/${rkey || Date.now().toString(36)}`; + const title = truncate(String(value.title || slug || "Untitled Paper"), 5000); + + return { + site: "https://papers.aesthetic.computer", + path, + title, + description: truncate( + `Paper from Aesthetic Computer${value.languages?.length ? ` (${value.languages.join(", ")})` : ""}`, + 3000, + ), + tags: ["paper", "aesthetic-computer"], + publishedAt: toIsoString(value.when), + }; + }, + }, + news: { + collection: "computer.aesthetic.news", + toDocument(sourceRecord) { + const value = sourceRecord.value || {}; + const rkey = rkeyFromUri(sourceRecord.uri); + const site = "https://news.aesthetic.computer"; + const pathFromLink = pathFromUrlIfSameBase(value.link, site); + const path = pathFromLink || `/atproto/${rkey || Date.now().toString(36)}`; + + const headline = String(value.headline || "").trim(); + if (!headline) return null; + + const body = String(value.body || "").trim(); + const tags = Array.isArray(value.tags) + ? value.tags + .map((tag) => String(tag || "").trim()) + .filter(Boolean) + .slice(0, 16) + : []; + + return { + site, + path, + title: truncate(headline, 5000), + description: truncate(body || `News update: ${headline}`, 3000), + textContent: body || undefined, + tags, + publishedAt: toIsoString(value.when), + }; + }, + }, + piece: { + collection: "computer.aesthetic.piece", + toDocument(sourceRecord) { + const value = sourceRecord.value || {}; + const slug = String(value.slug || "").trim(); + if (!slug) return null; + + return { + site: "https://aesthetic.computer", + path: `/${slug}`, + title: truncate(slug, 5000), + description: truncate(`Interactive piece: ${slug}`, 3000), + tags: ["piece", "interactive"], + publishedAt: toIsoString(value.when), + }; + }, + }, + kidlisp: { + collection: "computer.aesthetic.kidlisp", + toDocument(sourceRecord) { + const value = sourceRecord.value || {}; + const code = String(value.code || "").trim(); + if (!code) return null; + + const source = String(value.source || "").trim(); + return { + site: "https://aesthetic.computer", + path: `/$${code}`, + title: truncate(`KidLisp ${code}`, 5000), + description: truncate(`KidLisp program ${code}`, 3000), + textContent: source || undefined, + tags: ["kidlisp", "code"], + publishedAt: toIsoString(value.when), + }; + }, + }, + mood: { + collection: "computer.aesthetic.mood", + toDocument(sourceRecord) { + const value = sourceRecord.value || {}; + const mood = String(value.mood || "").trim(); + if (!mood) return null; + const rkey = rkeyFromUri(sourceRecord.uri); + const title = truncate(mood.slice(0, 120), 5000); + return { + site: "https://aesthetic.computer", + path: `/mood/${rkey || Date.now().toString(36)}`, + title, + description: truncate(mood, 3000), + textContent: mood, + tags: ["mood"], + publishedAt: toIsoString(value.when), + }; + }, + }, +}; + +function parseArgs(argv) { + const out = { _: [] }; + for (let i = 0; i < argv.length; i++) { + const token = argv[i]; + if (!token.startsWith("--")) { + out._.push(token); + continue; + } + const eq = token.indexOf("="); + if (eq !== -1) { + out[token.slice(2, eq)] = token.slice(eq + 1); + continue; + } + const next = argv[i + 1]; + if (next && !next.startsWith("--")) { + out[token.slice(2)] = next; + i++; + } else { + out[token.slice(2)] = true; + } + } + return out; +} + +function toIsoString(value) { + const date = value instanceof Date ? value : new Date(value || Date.now()); + if (!Number.isNaN(date.getTime())) return date.toISOString(); + return new Date().toISOString(); +} + +function truncate(value, max) { + const str = String(value || ""); + if (!max || str.length <= max) return str; + return str.slice(0, max); +} + +function trimTrailingSlash(value) { + return String(value || "").replace(/\/+$/, ""); +} + +function pathFromUrlIfSameBase(candidateUrl, baseUrl) { + if (!candidateUrl) return null; + try { + const base = new URL(trimTrailingSlash(baseUrl)); + const candidate = new URL(String(candidateUrl)); + if (candidate.origin !== base.origin) return null; + const normalizedBasePath = trimTrailingSlash(base.pathname || ""); + const normalizedCandidatePath = candidate.pathname || "/"; + + if ( + normalizedBasePath && + normalizedBasePath !== "/" && + !normalizedCandidatePath.startsWith(normalizedBasePath) + ) { + return null; + } + + const pathname = normalizedCandidatePath.startsWith("/") + ? normalizedCandidatePath + : `/${normalizedCandidatePath}`; + return `${pathname}${candidate.search || ""}${candidate.hash || ""}`; + } catch { + return null; + } +} + +function rkeyFromUri(uri) { + const str = String(uri || ""); + if (!str.includes("/")) return ""; + return str.split("/").pop() || ""; +} + +async function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function listAllRecords(agent, repo, collection, limit = null) { + const records = []; + let cursor; + + while (true) { + const remaining = limit == null ? MAX_PAGE_SIZE : Math.max(limit - records.length, 0); + if (remaining <= 0) break; + + const pageLimit = Math.min(MAX_PAGE_SIZE, remaining); + const response = await agent.com.atproto.repo.listRecords({ + repo, + collection, + limit: pageLimit, + cursor, + }); + + const batch = response.data?.records || []; + records.push(...batch); + + cursor = response.data?.cursor; + if (!cursor || batch.length === 0) break; + } + + return records; +} + +async function loadTargetUsers(database, handle, userLimit = null) { + const users = database.db.collection("users"); + const handles = database.db.collection("@handles"); + + if (handle) { + const clean = handle.replace(/^@/, ""); + const handleDoc = await handles.findOne({ handle: clean }); + if (!handleDoc) { + throw new Error(`User @${clean} not found`); + } + const user = await users.findOne({ _id: handleDoc._id }); + if (!user?.atproto?.did || !user?.atproto?.password) { + throw new Error(`User @${clean} has no ATProto credentials`); + } + return [ + { + sub: user._id, + handle: clean, + did: user.atproto.did, + password: user.atproto.password, + }, + ]; + } + + const userDocs = await users + .find({ + "atproto.did": { $exists: true, $ne: null }, + "atproto.password": { $exists: true, $ne: null }, + }) + .project({ _id: 1, atproto: 1 }) + .toArray(); + + const byId = new Map(); + const handleDocs = await handles + .find({ _id: { $in: userDocs.map((u) => u._id) } }) + .project({ _id: 1, handle: 1 }) + .toArray(); + + for (const doc of handleDocs) { + byId.set(String(doc._id), doc.handle || "unknown"); + } + + const allUsers = userDocs.map((user) => ({ + sub: user._id, + handle: byId.get(String(user._id)) || "unknown", + did: user.atproto.did, + password: user.atproto.password, + })); + + if (userLimit == null || Number.isNaN(userLimit) || userLimit <= 0) { + return allUsers; + } + + return allUsers.slice(0, userLimit); +} + +function parseSources(arg) { + if (!arg) return ["paper", "news", "piece"]; + return String(arg) + .split(",") + .map((source) => source.trim().toLowerCase()) + .filter(Boolean); +} + +function ensureSourcesValid(sourceNames) { + const invalid = sourceNames.filter((name) => !SOURCE_CONFIG[name]); + if (invalid.length > 0) { + throw new Error( + `Unknown sources: ${invalid.join(", ")}. Valid sources: ${Object.keys(SOURCE_CONFIG).join(", ")}`, + ); + } +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + const dryRun = Boolean(args["dry-run"]); + const targetHandle = args.user ? String(args.user) : null; + const userLimit = args["user-limit"] ? parseInt(args["user-limit"], 10) : null; + const sources = parseSources(args.sources); + const limit = args.limit ? parseInt(args.limit, 10) : null; + const batchSize = args["batch-size"] ? parseInt(args["batch-size"], 10) : 20; + const delayMs = args.delay ? parseInt(args.delay, 10) : 300; + + ensureSourcesValid(sources); + + console.log("\n🧬 Backfill Standard.site Documents\n"); + console.log(`PDS: ${PDS_URL}`); + console.log(`Mode: ${dryRun ? "🔍 DRY RUN" : "✍️ LIVE"}`); + if (targetHandle) console.log(`User: ${targetHandle}`); + if (!targetHandle && userLimit != null && !Number.isNaN(userLimit)) { + console.log(`User limit: ${userLimit}`); + } + console.log(`Sources: ${sources.join(", ")}`); + if (limit != null && !Number.isNaN(limit)) console.log(`Limit/source: ${limit}`); + console.log(`Batch size: ${batchSize}`); + console.log(`Delay: ${delayMs}ms\n`); + + const database = await connect(); + + let usersToProcess; + try { + usersToProcess = await loadTargetUsers(database, targetHandle, userLimit); + } catch (error) { + await database.disconnect(); + throw error; + } + + if (usersToProcess.length === 0) { + console.log("No users with ATProto credentials found."); + await database.disconnect(); + return; + } + + const totals = { + users: usersToProcess.length, + created: 0, + skipped: 0, + failed: 0, + }; + + const bySource = Object.fromEntries( + sources.map((source) => [source, { created: 0, skipped: 0, failed: 0, scanned: 0 }]), + ); + + for (let u = 0; u < usersToProcess.length; u++) { + const user = usersToProcess[u]; + console.log(`\n[${u + 1}/${usersToProcess.length}] @${user.handle} (${user.did})`); + + const agent = new AtpAgent({ service: PDS_URL }); + try { + await agent.login({ identifier: user.did, password: user.password }); + } catch (error) { + console.log(` ❌ Login failed: ${error.message}`); + totals.failed += 1; + continue; + } + + let existingDocs = []; + try { + existingDocs = await listAllRecords(agent, user.did, TARGET_COLLECTION, null); + } catch { + existingDocs = []; + } + + const existingBySourceUri = new Set( + existingDocs + .map((record) => String(record.value?.sourceAtUri || "").trim()) + .filter(Boolean), + ); + + console.log(` Existing ${TARGET_COLLECTION}: ${existingDocs.length}`); + + for (const sourceName of sources) { + const source = SOURCE_CONFIG[sourceName]; + + let sourceRecords = []; + try { + sourceRecords = await listAllRecords(agent, user.did, source.collection, limit); + } catch { + sourceRecords = []; + } + + if (sourceRecords.length === 0) { + console.log(` ${sourceName.padEnd(8)} 0 source records`); + continue; + } + + console.log(` ${sourceName.padEnd(8)} scanning ${sourceRecords.length} records`); + + for (let i = 0; i < sourceRecords.length; i++) { + const sourceRecord = sourceRecords[i]; + bySource[sourceName].scanned += 1; + + const sourceUri = String(sourceRecord.uri || "").trim(); + if (!sourceUri) { + totals.skipped += 1; + bySource[sourceName].skipped += 1; + continue; + } + + if (existingBySourceUri.has(sourceUri)) { + totals.skipped += 1; + bySource[sourceName].skipped += 1; + continue; + } + + const mapped = source.toDocument(sourceRecord); + if (!mapped || !mapped.site || !mapped.title || !mapped.publishedAt) { + totals.skipped += 1; + bySource[sourceName].skipped += 1; + continue; + } + + const payload = { + $type: TARGET_COLLECTION, + ...mapped, + sourceAtUri: sourceUri, + sourceCollection: source.collection, + }; + + const ref = sourceRecord.value?.ref; + if (typeof ref === "string" && ref.trim()) { + payload.sourceRef = ref.trim(); + } + + if (dryRun) { + console.log( + ` [dry] ${sourceName} → ${truncate(mapped.title, 64)} (${mapped.publishedAt.slice(0, 10)})`, + ); + totals.created += 1; + bySource[sourceName].created += 1; + existingBySourceUri.add(sourceUri); + continue; + } + + try { + const result = await agent.com.atproto.repo.createRecord({ + repo: user.did, + collection: TARGET_COLLECTION, + record: payload, + }); + + const uri = result.uri || result.data?.uri || ""; + const rkey = rkeyFromUri(uri); + console.log(` ✅ ${sourceName} → ${rkey}`); + + totals.created += 1; + bySource[sourceName].created += 1; + existingBySourceUri.add(sourceUri); + } catch (error) { + console.log(` ❌ ${sourceName}: ${error.message}`); + totals.failed += 1; + bySource[sourceName].failed += 1; + } + + if ((i + 1) % batchSize === 0 && i < sourceRecords.length - 1) { + await sleep(delayMs); + } + } + } + } + + console.log("\n" + "═".repeat(60)); + console.log("Standard.site Sync Summary\n"); + console.log(`Users processed: ${totals.users}`); + console.log(`Created: ${totals.created}`); + console.log(`Skipped: ${totals.skipped}`); + console.log(`Failed: ${totals.failed}\n`); + + for (const sourceName of sources) { + const stats = bySource[sourceName]; + console.log( + `${sourceName.padEnd(8)} scanned:${String(stats.scanned).padStart(5)} created:${String(stats.created).padStart(5)} skipped:${String(stats.skipped).padStart(5)} failed:${String(stats.failed).padStart(5)}`, + ); + } + + if (dryRun) { + console.log("\n💡 Dry run only. Re-run without --dry-run to create records."); + } + + console.log(); + await database.disconnect(); +} + +main() + .then(() => { + process.exit(0); + }) + .catch((error) => { + console.error(`\n❌ Fatal: ${error.message}`); + process.exit(1); + }); diff --git a/package.json b/package.json --- a/package.json +++ b/package.json @@ -51,6 +51,8 @@ "at:lexicons": "node at/cli.mjs lexicons", "at:invite": "node at/cli.mjs invite", "at:accounts": "node at/cli.mjs accounts", "at:sync": "node at/cli.mjs sync:status", + "at:standard:sync": "node at/scripts/atproto/backfill-standard-site-documents.mjs", + "at:standard:dry": "node at/scripts/atproto/backfill-standard-site-documents.mjs --dry-run", "profile:secret:rotate": "node utilities/rotate-profile-stream-secret.mjs", "ac": "./aesthetic", "admin:udp": "ssh root@157.245.134.225", diff --git a/papers/arxiv-identity/identity.tex b/papers/arxiv-identity/identity.tex new file mode 100644 --- /dev/null +++ b/papers/arxiv-identity/identity.tex @@ -0,0 +1,591 @@ +% !TEX program = xelatex +\documentclass[10pt,letterpaper,twocolumn]{article} + +% === GEOMETRY === +\usepackage[top=0.75in, bottom=0.75in, left=0.75in, right=0.75in]{geometry} + +% === FONTS === +\usepackage{fontspec} +\usepackage{unicode-math} + +\setmainfont{Latin Modern Roman} +\setsansfont{Latin Modern Sans} + +% Custom AC fonts +\newfontfamily\acbold{ywft-processing-bold}[ + Path=../../system/public/type/webfonts/, + Extension=.ttf +] +\newfontfamily\aclight{ywft-processing-light}[ + Path=../../system/public/type/webfonts/, + Extension=.ttf +] +\setmonofont{Latin Modern Mono}[Scale=0.85] + +% === PACKAGES === +\usepackage{xcolor} +\usepackage{titlesec} +\usepackage{enumitem} +\usepackage{booktabs} +\usepackage{tabularx} +\usepackage{multicol} +\usepackage{fancyhdr} +\usepackage{hyperref} +\usepackage{graphicx} +\graphicspath{{figures/}{../../papers/arxiv-ac/figures/}} +\usepackage{ragged2e} +\usepackage{microtype} +\usepackage{listings} +\usepackage{natbib} +\usepackage[colorspec=0.92]{draftwatermark} + +% === COLORS (AC palette) === +\definecolor{acpink}{RGB}{180,72,135} +\definecolor{acpurple}{RGB}{120,80,180} +\definecolor{acdark}{RGB}{64,56,74} +\definecolor{acgray}{RGB}{119,119,119} +\definecolor{draftcolor}{RGB}{180,72,135} + +% === DRAFT WATERMARK === +\DraftwatermarkOptions{ + text=WORKING DRAFT, + fontsize=3cm, + color=draftcolor!18, + angle=45, + pos={0.5\paperwidth, 0.5\paperheight} +} + +% === JS SYNTAX COLORS === +\definecolor{jskw}{RGB}{119,51,170} +\definecolor{jsfn}{RGB}{0,136,170} +\definecolor{jsstr}{RGB}{170,120,0} +\definecolor{jsnum}{RGB}{204,0,102} +\definecolor{jscmt}{RGB}{102,102,102} + +% === HYPERREF === +\hypersetup{ + colorlinks=true, + linkcolor=acpurple, + urlcolor=acpurple, + citecolor=acpurple, + pdfauthor={@jeffrey}, + pdftitle={Handle Identity on the AT Protocol: From Auth0 to Decentralized Sign-In}, +} + +% === SECTION FORMATTING === +\titleformat{\section} + {\normalfont\bfseries\normalsize\uppercase} + {\thesection.} + {0.5em} + {} +\titlespacing{\section}{0pt}{1.2em}{0.3em} + +\titleformat{\subsection} + {\normalfont\bfseries\small} + {\thesubsection} + {0.5em} + {} +\titlespacing{\subsection}{0pt}{0.8em}{0.2em} + +% === HEADER/FOOTER === +\pagestyle{fancy} +\fancyhf{} +\renewcommand{\headrulewidth}{0pt} +\fancyhead[C]{\footnotesize\color{acpink}\textit{Working Draft --- not for citation}} +\fancyfoot[C]{\footnotesize\thepage} + +% === CUSTOM COMMANDS === +\newcommand{\acdot}{{\color{acpink}.}} +\newcommand{\ac}{\textsc{Aesthetic.Computer}} +\newcommand{\atproto}{\textsc{AT Protocol}} + +% Random caps for Aesthetic.Computer branding +\newcount\acrandtmp +\newcommand{\acrandletter}[2]{% + \acrandtmp=\uniformdeviate 2\relax + \ifnum\acrandtmp=0\relax#1\else#2\fi% +} +\newcommand{\acrandname}{% + \acrandletter{a}{A}\acrandletter{e}{E}\acrandletter{s}{S}\acrandletter{t}{T}% + \acrandletter{h}{H}\acrandletter{e}{E}\acrandletter{t}{T}\acrandletter{i}{I}% + \acrandletter{c}{C}{\color{acpink}.}\acrandletter{c}{C}\acrandletter{o}{O}% + \acrandletter{m}{M}\acrandletter{p}{P}\acrandletter{u}{U}\acrandletter{t}{T}% + \acrandletter{e}{E}\acrandletter{r}{R}% +} + +% === LISTINGS === +\lstdefinelanguage{acjs}{ + morekeywords=[1]{function,export,const,let,var,return,if,else,new,async,await,import,from}, + morekeywords=[2]{wipe,ink,line,box,circle,write,screen,params,colon,jump,send,store,net,sound,speaker,system}, + sensitive=true, + morecomment=[l]{//}, + morestring=[b]", + morestring=[b]', + morestring=[b]`, + escapeinside={|}{|}, +} + +\lstdefinestyle{acjsstyle}{ + language=acjs, + keywordstyle=[1]\color{jskw}\bfseries, + keywordstyle=[2]\color{jsfn}\bfseries, + commentstyle=\color{jscmt}\itshape, + stringstyle=\color{jsstr}, +} + +\lstset{ + basicstyle=\ttfamily\small, + breaklines=true, + frame=single, + rulecolor=\color{acgray!30}, + backgroundcolor=\color{acgray!5}, + xleftmargin=0.5em, + xrightmargin=0.5em, + aboveskip=0.5em, + belowskip=0.5em, +} + +% === LIST SETTINGS === +\setlist[itemize]{nosep, leftmargin=1.2em, itemsep=0.1em} +\setlist[enumerate]{nosep, leftmargin=1.2em} + +% === COLUMN SEPARATION === +\setlength{\columnsep}{1.8em} + +% === PARAGRAPH SETTINGS === +\setlength{\parindent}{1em} +\setlength{\parskip}{0.3em} + +% Hyphenation for narrow two-column layout +\tolerance=800 +\emergencystretch=1em +\hyphenpenalty=50 + +\begin{document} + +% ============ TITLE BLOCK ============ + +\twocolumn[{% +\begin{center} +\includegraphics[height=4em]{pals}\par\vspace{0.5em} +{\acbold\fontsize{24pt}{28pt}\selectfont\color{acdark} Handle Identity on the AT Protocol}\par +\vspace{0.2em} +{\aclight\fontsize{11pt}{13pt}\selectfont\color{acpink} From Auth0 to Decentralized Sign-In on Aesthetic Computer}\par +\vspace{0.3em} +{\aclight\fontsize{9pt}{11pt}\selectfont\color{acgray} ATProto OAuth, Handle Verification, and Portable Creative Identity}\par +\vspace{0.6em} +{\normalsize @jeffrey}\par +{\small\color{acgray} Aesthetic.Computer}\par +{\small\color{acgray} ORCID: \href{https://orcid.org/0009-0007-4460-4913}{0009-0007-4460-4913}}\par +\vspace{0.3em} +{\small\color{acpurple} \url{https://aesthetic.computer}}\par +\vspace{0.6em} +\rule{\textwidth}{1.5pt} +\vspace{0.5em} +\end{center} + +\begin{center} +{\small\color{acpink}\textbf{[ working draft --- not for citation ]}} +\end{center} +\vspace{0.3em} + +\begin{quote} +\small\noindent\textbf{Abstract.} +Aesthetic Computer currently authenticates users through Auth0 and maintains a parallel identity on a self-hosted AT Protocol Personal Data Server (PDS) at \texttt{at.aesthetic.computer}. Each verified user receives a DID and a PDS handle, but authentication flows through a centralized OAuth provider. We propose collapsing this dual-identity architecture by adopting AT Protocol OAuth~\citep{atproto2024oauth} as a primary sign-in method, allowing anyone with a Bluesky or ATProto identity to authenticate directly and claim the equivalent handle on Aesthetic Computer. This paper surveys the \atproto{} identity stack---DIDs, handle verification, OAuth 2.1 with DPoP and PAR---examines how pckt.blog and other ATProto-native applications implement decentralized sign-in~\citep{pcktblog2025}, maps the current AC authentication architecture, and proposes a phased migration from Auth0 dependency to \atproto{}-first identity. The central argument: on a platform that already runs its own PDS and mints its own DIDs, the centralized identity provider is the vestigial organ. Removing it simplifies the stack, eliminates a paid dependency, and makes AC a first-class citizen of the federated social web. +\end{quote} +\vspace{0.5em} +}] + +% ============ 1. INTRODUCTION ============ + +\section{Introduction} + +Identity on the web is a landlord problem. You do not own your handle on Twitter, your username on Instagram, or your login on any platform that can delete your account. The AT Protocol~\citep{atproto2024spec}---the decentralized social networking protocol behind Bluesky---proposes a different arrangement: your identity is a cryptographic key pair, your handle is a domain name you control, and your data lives on a Personal Data Server that you can move between providers. + +Aesthetic Computer has operated a hybrid identity system since 2024. Auth0~\citep{auth0spa} handles authentication: OAuth 2.0 with PKCE, refresh tokens, and a managed user database. Separately, a self-hosted PDS at \texttt{at.aesthetic.computer} mints ATProto identities for every verified user. The result is a doubled system: two identities per user, two credential stores, two handle sync paths, and a paid Auth0 subscription bridging the gap. + +This paper asks: what if the PDS \emph{is} the identity provider? + +The answer is not hypothetical. pckt.blog~\citep{pcktblog2025}, a blogging platform built on the \atproto{}, authenticates users entirely through \atproto{} OAuth. Users sign in with their Bluesky handle, their self-hosted PDS, or any compatible identity provider. No Auth0, no Firebase, no centralized user database. The content syncs to the user's own PDS using shared lexicons from Standard.site~\citep{standardsite2025}. + +We examine how this model applies to Aesthetic Computer---a creative computing platform with 600+ interactive pieces, multiplayer sessions, a KidLisp programming language, and a native bare-metal OS~\citep{scudder2026os}---and propose a phased migration that preserves backward compatibility while moving toward decentralized identity. + +% ============ 2. THE CURRENT AC IDENTITY ARCHITECTURE ============ + +\section{Current Architecture} +\label{sec:current} + +\subsection{Auth0 as Identity Provider} + +Authentication flows through Auth0's SPA SDK. On page load, \texttt{boot.mjs} checks localStorage for cached Auth0 state. If a session exists (or an OAuth callback is detected), it initializes the Auth0 client with PKCE and refresh tokens, exchanges authorization codes for access tokens, and retrieves the user profile. The Auth0 \texttt{sub} field (e.g., \texttt{auth0|abc123} or \texttt{google-oauth2|xyz}) serves as the internal user identifier. + +\begin{lstlisting}[style=acjsstyle] +// boot.mjs: current Auth0 flow +const auth0 = await createAuth0Client({ + domain: |\textcolor{jsstr}{"hi.aesthetic.computer"}|, + clientId: |\textcolor{jsstr}{"LVdZaM..."}|, + cacheLocation: |\textcolor{jsstr}{"localstorage"}|, + useRefreshTokens: true, +}); +const user = await auth0.getUser(); +// { sub, email, email_verified, ... } +\end{lstlisting} + +\subsection{Handle System} + +Handles are stored in a MongoDB \texttt{@handles} collection, mapping Auth0 \texttt{sub} to a 1--16 character alphanumeric string (with \texttt{.} and \texttt{\_} allowed). Validation enforces no leading/trailing punctuation, case-insensitive uniqueness, and profanity filtering. Handles are the primary user-facing identity: URL-addressable (\texttt{@handle/piece-name}), visible in chat, and spoken aloud by AC Native OS. + +\subsection{ATProto Shadow Identity} + +On first email verification, an Auth0 webhook triggers \texttt{createAtprotoAccount()}, which: + +\begin{enumerate} + \item Generates a 32-character password + \item Creates an invite code on the PDS + \item Creates an account at \texttt{handle.at.aesthetic.computer} + \item Stores the DID, handle, and encrypted password in MongoDB (\texttt{users.atproto}) +\end{enumerate} + +When a user changes their AC handle, \texttt{updateAtprotoHandle()} syncs the change to the PDS. Content (paintings, moods, KidLisp snippets, tapes, news) is mirrored to the PDS via six custom lexicons (\texttt{computer.aesthetic.*}). The ATProto identity is real and functional---but the user never authenticates through it. It is a shadow identity: present, synced, but not sovereign. + +\subsection{The Duplication Problem} + +This architecture means: + +\begin{itemize} + \item Two credential stores (Auth0 + PDS) + \item Two handle namespaces (AC handles + PDS handles) + \item Two sync paths (handle changes propagate Auth0 $\to$ MongoDB $\to$ PDS) + \item A paid dependency (Auth0 subscription) + \item No interoperability (a Bluesky user cannot sign into AC with their existing identity) +\end{itemize} + +The PDS already knows who each user is. It already stores their DID, their handle, their content. The Auth0 layer adds cost and complexity without adding capability that the PDS cannot provide. + +% ============ 3. THE AT PROTOCOL IDENTITY STACK ============ + +\section{The AT Protocol Identity Stack} +\label{sec:atproto} + +Understanding the migration requires understanding the three layers of \atproto{} identity: DIDs, handles, and OAuth. + +\subsection{Decentralized Identifiers (DIDs)} + +A DID~\citep{w3cdid2022, atproto2024did} is a persistent, cryptographically verifiable identifier. The \atproto{} primarily uses \texttt{did:plc}, a method designed for strong consistency, high availability, and key rotation without losing identity. Each DID resolves to a document containing: + +\begin{itemize} + \item A \textbf{signing key} (P-256 or K-256)~\citep{atproto2024crypto} for authenticating repository updates + \item \textbf{Rotation keys} for account recovery + \item A \textbf{PDS endpoint} URL + \item The user's current \textbf{handle} +\end{itemize} + +The DID is the stable identity. Handles change; keys rotate; PDS providers come and go. The DID persists. AC already mints DIDs for every user through its PDS. The infrastructure exists. + +\subsection{Handle Verification} + +An \atproto{} handle~\citep{atproto2024handle} is a domain name that bidirectionally resolves to a DID. Verification uses two methods: + +\textbf{DNS TXT}: Place a record at \texttt{\_atproto.example.com}: +\begin{lstlisting} +_atproto.example.com TXT "did=did:plc:abc..." +\end{lstlisting} + +\textbf{HTTPS Well-Known}: Serve the DID at: +\begin{lstlisting} +GET /.well-known/atproto-did +Response: did:plc:abc... +\end{lstlisting} + +Both methods require the handle to resolve to the DID \emph{and} the DID document to claim the handle back. This bidirectional verification means: if you control the domain, you control the handle. No central authority assigns handles; DNS is the authority. + +For AC, this means \texttt{jeffrey.at.aesthetic.computer} is a real, verifiable ATProto handle because AC controls both the DNS and the PDS. But it also means a user who already owns \texttt{alice.bsky.social} or \texttt{alice.dev} has a cryptographically verified identity that AC can trust without a password. + +\subsection{ATProto OAuth} + +The \atproto{} OAuth specification~\citep{atproto2024oauth} extends OAuth 2.1 with three mandatory security mechanisms: + +\textbf{PKCE} (Proof Key for Code Exchange)~\citep{rfc7636pkce}: Prevents authorization code interception. The client generates a random verifier, sends its hash with the authorization request, and proves possession of the original verifier during token exchange. + +\textbf{PAR} (Pushed Authorization Requests)~\citep{rfc9126par}: The client submits authorization parameters via POST to the authorization server \emph{before} redirecting the user. This prevents parameter tampering in the redirect URL. + +\textbf{DPoP} (Demonstrating Proof of Possession)~\citep{rfc9449dpop}: Each request includes a signed JWT proving the client holds the private key associated with the token. Even if an access token leaks, it cannot be used by a different client. + +\subsubsection{Client Identification} + +Unlike traditional OAuth, \atproto{} does not require pre-registration with each authorization server. Instead, clients publish metadata at a public HTTPS URL: + +\begin{lstlisting}[style=acjsstyle] +// aesthetic.computer/oauth/client-metadata.json +{ + |\textcolor{jsstr}{"client\_id"}|: |\textcolor{jsstr}{"https://aesthetic.computer/..."}|, + |\textcolor{jsstr}{"client\_name"}|: |\textcolor{jsstr}{"Aesthetic Computer"}|, + |\textcolor{jsstr}{"redirect\_uris"}|: [|\textcolor{jsstr}{"https://..."}|], + |\textcolor{jsstr}{"grant\_types"}|: [|\textcolor{jsstr}{"authorization\_code"}|], + |\textcolor{jsstr}{"dpop\_bound\_access\_tokens"}|: true +} +\end{lstlisting} + +Any PDS can discover and verify the client by fetching this URL. No pre-shared secrets, no app store registration, no API key management. + +\subsubsection{The Flow} + +\begin{enumerate} + \item User enters their handle (e.g., \texttt{alice.bsky.social}) + \item Client resolves handle $\to$ DID $\to$ PDS endpoint $\to$ authorization server + \item Client pushes authorization parameters via PAR + \item User is redirected to their PDS's authorization UI + \item User approves; PDS redirects back with authorization code + \item Client exchanges code for DPoP-bound access token + \item Client verifies the returned DID matches the expected identity +\end{enumerate} + +The critical difference from Auth0: the user authenticates with \emph{their own server}. AC never sees a password. The PDS---whether Bluesky's, AC's own, or a self-hosted instance---is the identity authority. + +% ============ 4. HOW PCKT.BLOG DOES IT ============ + +\section{Case Study: pckt.blog} +\label{sec:pckt} + +pckt.blog~\citep{pcktblog2025} is a blogging platform that authenticates exclusively through \atproto{} OAuth. Its implementation demonstrates the practical viability of ATProto-only authentication for a content platform. + +\subsection{Authentication} + +Users sign in by entering their ATProto handle. pckt.blog resolves the handle, discovers the authorization server, and initiates the OAuth flow. The user approves on their PDS (Bluesky, Blacksky, a self-hosted server). pckt.blog receives a DPoP-bound access token and the user's DID. No passwords are stored. No separate user database is maintained. + +\subsection{Data Sovereignty} + +Content syncs to the user's own PDS using Standard.site lexicons~\citep{standardsite2025}: + +\begin{itemize} + \item \texttt{site.standard.publication} --- blog collections + \item \texttt{site.standard.document} --- individual articles + \item \texttt{site.standard.graph.subscription} --- follow relationships +\end{itemize} + +If pckt.blog disappears, the user's content remains on their PDS, accessible to any compatible reader. This is the promise of ATProto: the platform is a view, not a silo. + +\subsection{Implications for AC} + +pckt.blog proves that a content-oriented platform can operate entirely on ATProto identity. The parallels to AC are direct: + +\begin{itemize} + \item pckt.blog publishes articles; AC publishes pieces, paintings, and moods + \item pckt.blog uses Standard.site lexicons; AC has six custom \texttt{computer.aesthetic.*} lexicons + \item pckt.blog's users own their content on their PDS; AC already mirrors content to its PDS + \item Both are Node.js/JavaScript applications +\end{itemize} + +The gap: pckt.blog was built ATProto-native. AC has an existing Auth0 user base that must be migrated gracefully. + +% ============ 5. PROPOSED MIGRATION ============ + +\section{Proposed Migration} +\label{sec:migration} + +\subsection{Phase 1: ATProto OAuth as Secondary Sign-In} + +Add ``Sign in with Bluesky'' alongside Auth0, without removing Auth0. + +\textbf{Infrastructure:} +\begin{enumerate} + \item Publish client metadata at \texttt{aesthetic.computer/oauth/client-metadata.json} + \item Add \texttt{@atproto/oauth-client-node}~\citep{npmAtprotoOauth} to the backend + \item Create two new Netlify functions: + \begin{itemize} + \item \texttt{POST /api/atproto-auth/start} --- resolve handle, PAR, redirect + \item \texttt{GET /api/atproto-auth/callback} --- code exchange with DPoP + \end{itemize} + \item Store sessions in Redis (DID, handle, DPoP key pair, tokens) +\end{enumerate} + +\textbf{Client flow:} A new ``Sign in with Bluesky'' button in \texttt{boot.mjs} triggers the ATProto OAuth flow. On success, \texttt{window.acUSER} is populated from the ATProto session (DID as \texttt{sub}, handle, no email unless the user provides one). The piece API surface is identical---pieces see a user with a handle, regardless of which auth path created the session. + +\subsection{Phase 2: Handle Bridging} + +When a user signs in via ATProto, bridge their handle to AC: + +\begin{enumerate} + \item Extract the username from the ATProto handle (e.g., \texttt{alice} from \texttt{alice.bsky.social}) + \item Check availability against the AC \texttt{@handles} collection + \item If available and valid (1--16 chars, alphanumeric), offer one-click claim + \item If taken, check if the existing owner's email matches---offer account linking + \item If taken by someone else, prompt for an alternative + \item Store a DID $\leftrightarrow$ AC sub mapping in MongoDB for future sign-ins +\end{enumerate} + +Custom domain handles (e.g., \texttt{alice.dev}) require the user to choose an AC handle manually, since the domain itself may not map to a valid handle string. + +\subsection{Phase 3: Identity Linking} + +Existing Auth0 users can link their ATProto identity: + +\begin{enumerate} + \item From account settings, initiate ATProto OAuth + \item On success, store the external DID alongside the Auth0 sub + \item Future sign-ins accept either auth path + \item Content can optionally sync to the user's external PDS (not just AC's PDS) +\end{enumerate} + +This phase turns the existing \texttt{users.atproto} field from a shadow identity into a first-class identity link. + +\subsection{Phase 4: ATProto-Primary} + +Once the migration is validated: + +\begin{enumerate} + \item New signups default to ATProto OAuth (creating accounts on AC's PDS) + \item Auth0 remains as a legacy path for existing users + \item Gradually sunset Auth0 as users link their ATProto identities + \item Remove Auth0 dependency, eliminate subscription cost +\end{enumerate} + +\subsection{PDS Routing} + +A key architectural decision: where does content go? + +\begin{itemize} + \item \textbf{Auth0-only users}: content syncs to AC's PDS (current behavior) + \item \textbf{ATProto users with external PDS}: content syncs to \emph{their} PDS + \item \textbf{ATProto users on AC's PDS}: content stays on AC's PDS +\end{itemize} + +This means the backend sync functions (\texttt{media-atproto.mjs}, \texttt{painting-atproto.mjs}, etc.) need a conditional path: resolve the user's PDS endpoint from their DID document, and write to that endpoint rather than assuming AC's PDS. + +% ============ 6. HANDLE SEMANTICS ============ + +\section{Handle Semantics} +\label{sec:handles} + +The handle is the most human-visible piece of the identity stack, and the migration raises questions about what a handle \emph{means}. + +\subsection{Current Handle Model} + +Today, an AC handle is: +\begin{itemize} + \item A 1--16 character string, first-come-first-served + \item Unique within the AC namespace + \item Used for URLs (\texttt{@alice/painting}), chat, and OS personalization + \item Stored in MongoDB, cached in Redis + \item Mirrored to the PDS as \texttt{alice.at.aesthetic.computer} +\end{itemize} + +\subsection{ATProto Handle Model} + +An ATProto handle is: +\begin{itemize} + \item A domain name (any valid DNS name) + \item Verified bidirectionally against a DID + \item Globally unique by DNS authority + \item Portable across services +\end{itemize} + +\subsection{Bridging the Models} + +The proposal: an AC handle is an \emph{alias} that maps to a DID. The source of truth shifts from MongoDB to the DID layer. Multiple sign-in methods (Auth0, ATProto OAuth) resolve to the same DID, which resolves to the same AC handle. + +For AC Native OS~\citep{scudder2026os}, where the handle is inscribed in \texttt{config.json} on the boot partition, this means the device identity is backed by a cryptographic identity. ``Hi @alice'' on the boot screen means Alice's DID, Alice's signing key, Alice's portable identity---not just a string in a config file. + +% ============ 7. OPEN QUESTIONS ============ + +\section{Open Questions} +\label{sec:questions} + +\textbf{Handle priority.} If \texttt{alice} is unclaimed on AC but \texttt{alice.bsky.social} signs in, should she get it automatically? First-come-first-served is simple but allows squatting. ATProto-verified priority is fairer but adds complexity. + +\textbf{Email requirement.} Auth0 provides verified email for account recovery. ATProto OAuth does not guarantee email. Should AC require an email for full account features (purchasing, notifications)? + +\textbf{Multi-tenant.} AC operates a second tenant (\texttt{sotce}) with separate Auth0. How do ATProto identities map across tenants? The DID is tenant-agnostic, which may simplify cross-tenant identity. + +\textbf{Session server.} Real-time features (chat, multiplayer) authenticate via Auth0 tokens forwarded through WebSocket. The session server must accept ATProto-issued tokens or a unified session token. + +\textbf{Device pairing.} AC Native OS pairs via a 6-character code exchanged through Auth0. The ATProto equivalent: scan a QR code that initiates an OAuth flow on the phone, delivering tokens to the device. + +\textbf{Admin identity.} Admin is currently \texttt{handle === "jeffrey" \&\& sub === ADMIN\_SUB}. Under ATProto-first auth, admin is \texttt{did === admin\_did}---cleaner, cryptographically grounded. + +% ============ 8. RELATED WORK ============ + +\section{Related Work} +\label{sec:related} + +\textbf{Decentralized identity.} The W3C DID specification~\citep{w3cdid2022} provides the formal framework for decentralized identifiers. The AT Protocol's \texttt{did:plc} method~\citep{atproto2024did} extends this with strong consistency guarantees and a centralized-but-auditable directory at \texttt{plc.directory}. This is a pragmatic compromise: full decentralization of identity resolution remains an open problem, and \texttt{did:plc} trades some decentralization for operational reliability. + +\textbf{ATProto-native applications.} pckt.blog~\citep{pcktblog2025} demonstrates blogging; Leaflet.pub and Offprint.app collaborate on shared lexicons via Standard.site~\citep{standardsite2025}. These applications prove the viability of ATProto-only auth for content platforms. + +\textbf{OAuth security.} DPoP~\citep{rfc9449dpop} prevents token theft; PAR~\citep{rfc9126par} prevents parameter tampering; PKCE~\citep{rfc7636pkce} prevents code interception. Together they represent the state of the art in browser-based OAuth security---significantly stronger than the Auth0 SPA flow AC currently uses. + +\textbf{Convivial identity.} Illich's tools for conviviality~\citep{illich1973tools} frame the question: does the identity system expand personal autonomy, or does it require dependence on a provider? Auth0 is a managed service---convenient but dependent. ATProto identity is portable, self-verifiable, and provider-independent. Nelson's vision of personal computing~\citep{nelson1974computerlib} extends naturally to personal identity: you should own your name on the network. + +% ============ 9. CONCLUSION ============ + +\section{Conclusion} + +Aesthetic Computer already runs a PDS, mints DIDs, syncs content to ATProto records, and publishes custom lexicons. The only piece missing is letting users authenticate through that infrastructure instead of routing through Auth0. The migration is not a rewrite---it is the removal of a workaround. + +The phased approach (ATProto as secondary sign-in $\to$ handle bridging $\to$ identity linking $\to$ ATProto-primary) ensures no existing user is disrupted. The end state: a creative computing platform where your handle is a cryptographic identity, your content lives on a server you control, and signing in means proving you own your keys---not trusting a third party to vouch for you. + +The PDS is already running. The DIDs are already minted. The lexicons are already published. It is time to let users sign in through the front door. + +% ============ REFERENCES ============ + +\vspace{0.5em} +\noindent\rule{\columnwidth}{0.5pt} + +\subsection*{Reference Links} + +\noindent\small + +\textbf{AT Protocol Specifications:} +\begin{itemize} + \item \url{https://atproto.com/specs} --- Full protocol specification + \item \url{https://atproto.com/specs/oauth} --- OAuth specification + \item \url{https://atproto.com/specs/handle} --- Handle resolution + \item \url{https://atproto.com/specs/cryptography} --- Cryptographic methods + \item \url{https://web.plc.directory/spec/v0.1/did-plc} --- DID PLC specification + \item \url{https://atproto.com/guides/lexicon} --- Lexicon schema system + \item \url{https://atproto.com/guides/oauth-patterns} --- OAuth implementation patterns +\end{itemize} + +\textbf{Implementation Guides:} +\begin{itemize} + \item \url{https://docs.bsky.app/blog/oauth-atproto} --- Building ATProto OAuth apps + \item \url{https://docs.bsky.app/docs/advanced-guides/resolving-identities} --- Identity resolution + \item \url{https://docs.bsky.app/docs/advanced-guides/oauth-client} --- OAuth client guide +\end{itemize} + +\textbf{NPM Packages:} +\begin{itemize} + \item \texttt{@atproto/oauth-client-node} --- Node.js OAuth client + \item \texttt{@atproto/oauth-client-browser} --- Browser OAuth client + \item \texttt{@atproto/api} --- TypeScript XRPC client + \item \texttt{@atproto/identity} --- DID/handle resolution + \item \texttt{@atcute/oauth-browser-client} --- Lightweight alternative +\end{itemize} + +\textbf{Reference Implementations:} +\begin{itemize} + \item \url{https://github.com/pilcrowonpaper/atproto-oauth-example} --- Astro OAuth example + \item \url{https://github.com/bluesky-social/atproto} --- Official ATProto monorepo + \item \url{https://standard.site/docs/introduction/} --- Standard.site shared lexicons +\end{itemize} + +\textbf{Applications Using ATProto Auth:} +\begin{itemize} + \item pckt.blog --- Blogging on the open social web + \item Leaflet.pub --- Long-form publishing + \item Offprint.app --- Collaborative writing +\end{itemize} + +\textbf{IETF Standards:} +\begin{itemize} + \item RFC 9449 --- DPoP (Demonstrating Proof of Possession) + \item RFC 7636 --- PKCE (Proof Key for Code Exchange) + \item RFC 9126 --- PAR (Pushed Authorization Requests) +\end{itemize} + +\vspace{0.5em} + +\bibliographystyle{plainnat} +\bibliography{references} + +\end{document} diff --git a/papers/arxiv-identity/references.bib b/papers/arxiv-identity/references.bib new file mode 100644 --- /dev/null +++ b/papers/arxiv-identity/references.bib @@ -0,0 +1,189 @@ +@misc{scudder2026ac, + title={Aesthetic Computer '26: A Mobile-First Runtime for Creative Computing}, + author={{@jeffrey}}, + year={2026}, + note={Companion paper describing the AC platform} +} + +@misc{scudder2026os, + title={AC Native OS '26: A Bare-Metal Creative Computing Operating System}, + author={{@jeffrey}}, + year={2026}, + note={Companion paper describing the AC Native OS} +} + +@misc{atproto2024spec, + title={AT Protocol Specification}, + author={{Bluesky PBC}}, + year={2024}, + howpublished={\url{https://atproto.com/specs}}, + note={Decentralized social networking protocol} +} + +@misc{atproto2024oauth, + title={AT Protocol OAuth Specification}, + author={{Bluesky PBC}}, + year={2024}, + howpublished={\url{https://atproto.com/specs/oauth}}, + note={OAuth 2.1 with PKCE, DPoP, and PAR for decentralized authentication} +} + +@misc{atproto2024handle, + title={AT Protocol Handle Specification}, + author={{Bluesky PBC}}, + year={2024}, + howpublished={\url{https://atproto.com/specs/handle}}, + note={Handle resolution via DNS TXT and HTTPS well-known} +} + +@misc{atproto2024did, + title={DID PLC Specification v0.1}, + author={{Bluesky PBC}}, + year={2024}, + howpublished={\url{https://web.plc.directory/spec/v0.1/did-plc}}, + note={Decentralized Identifiers for AT Protocol} +} + +@misc{atproto2024crypto, + title={AT Protocol Cryptography Specification}, + author={{Bluesky PBC}}, + year={2024}, + howpublished={\url{https://atproto.com/specs/cryptography}}, + note={P-256 and K-256 elliptic curve signing} +} + +@misc{bluesky2024oauth, + title={OAuth for AT Protocol: Building Atproto Apps}, + author={{Bluesky PBC}}, + year={2024}, + howpublished={\url{https://docs.bsky.app/blog/oauth-atproto}}, + note={Implementation guide for atproto OAuth clients} +} + +@misc{bluesky2024resolving, + title={Resolving Bluesky Identities}, + author={{Bluesky PBC}}, + year={2024}, + howpublished={\url{https://docs.bsky.app/docs/advanced-guides/resolving-identities}}, + note={Guide to DID resolution and handle verification} +} + +@misc{standardsite2025, + title={Standard Site: Open Blog Publishing on AT Protocol}, + author={{Standard.site}}, + year={2025}, + howpublished={\url{https://standard.site/docs/introduction/}}, + note={Shared lexicons for blog content portability across ATProto} +} + +@misc{pcktblog2025, + title={Blogging on the Open Social Web and More Exciting Features}, + author={{pckt.blog}}, + year={2025}, + howpublished={\url{https://devlog.pckt.blog/blogging-on-the-open-social-web-and-more-exciting-features-g9x55y6}}, + note={Development journal on ATProto blog integration} +} + +@misc{rfc9449dpop, + title={{RFC 9449: OAuth 2.0 Demonstrating Proof of Possession (DPoP)}}, + author={Fett, Daniel and Campbell, Brian and Bradley, John and Lodderstedt, Torsten and Jones, Michael and Waite, David}, + year={2023}, + howpublished={\url{https://datatracker.ietf.org/doc/html/rfc9449}}, + note={IETF standard for binding tokens to client key pairs} +} + +@misc{rfc7636pkce, + title={{RFC 7636: Proof Key for Code Exchange by OAuth Public Clients}}, + author={Sakimura, Nat and Bradley, John and Agarwal, Naveen}, + year={2015}, + howpublished={\url{https://datatracker.ietf.org/doc/html/rfc7636}}, + note={PKCE for OAuth authorization code flow} +} + +@misc{rfc9126par, + title={{RFC 9126: OAuth 2.0 Pushed Authorization Requests}}, + author={Lodderstedt, Torsten and Campbell, Brian and Sakimura, Nat and Tonge, Dave and Fett, Daniel}, + year={2021}, + howpublished={\url{https://datatracker.ietf.org/doc/html/rfc9126}}, + note={Pushed Authorization Requests for OAuth} +} + +@misc{pilcrow2024example, + title={AT Protocol OAuth Example (Astro/Runtime-Agnostic)}, + author={{pilcrowonpaper}}, + year={2024}, + howpublished={\url{https://github.com/pilcrowonpaper/atproto-oauth-example}}, + note={Reference implementation for atproto OAuth client} +} + +@misc{atprotopatterns2024, + title={AT Protocol OAuth Patterns}, + author={{Bluesky PBC}}, + year={2024}, + howpublished={\url{https://atproto.com/guides/oauth-patterns}}, + note={Public client, BFF, and TMB patterns for atproto OAuth} +} + +@book{nelson1974computerlib, + title={Computer Lib / Dream Machines}, + author={Nelson, Ted}, + year={1974}, + publisher={Self-published}, + note={Revised edition 1987, Tempus Books/Microsoft Press} +} + +@book{illich1973tools, + title={Tools for Conviviality}, + author={Illich, Ivan}, + year={1973}, + publisher={Harper \& Row}, + address={New York} +} + +@misc{w3cdid2022, + title={{Decentralized Identifiers (DIDs) v1.0}}, + author={{W3C}}, + year={2022}, + howpublished={\url{https://www.w3.org/TR/did-core/}}, + note={W3C Recommendation for decentralized identifiers} +} + +@misc{auth0spa, + title={{Auth0 Single Page App SDK}}, + author={{Auth0 by Okta}}, + year={2024}, + howpublished={\url{https://auth0.com/docs/libraries/auth0-single-page-app-sdk}}, + note={JavaScript SDK for browser-based OAuth flows} +} + +@misc{npmAtprotoOauth, + title={@atproto/oauth-client-node}, + author={{Bluesky PBC}}, + year={2024}, + howpublished={\url{https://www.npmjs.com/package/@atproto/oauth-client-node}}, + note={Node.js AT Protocol OAuth client library} +} + +@misc{npmAtprotoApi, + title={@atproto/api}, + author={{Bluesky PBC}}, + year={2024}, + howpublished={\url{https://www.npmjs.com/package/@atproto/api}}, + note={TypeScript client for AT Protocol XRPC APIs} +} + +@misc{atcuteOauth, + title={@atcute/oauth-browser-client}, + author={{mary-ext}}, + year={2024}, + howpublished={\url{https://github.com/mary-ext/atcute}}, + note={Lightweight alternative atproto OAuth browser client} +} + +@misc{atprotoLexicon, + title={AT Protocol Lexicon Guide}, + author={{Bluesky PBC}}, + year={2024}, + howpublished={\url{https://atproto.com/guides/lexicon}}, + note={Schema definition system for ATProto records} +} diff --git a/papers/cli.mjs b/papers/cli.mjs --- a/papers/cli.mjs +++ b/papers/cli.mjs @@ -170,6 +170,11 @@ base: "futures", siteName: "five-years-from-now-26-arxiv", title: "Five Years from Now", }, + "arxiv-identity": { + base: "identity", + siteName: "handle-identity-atproto-26-arxiv", + title: "Handle Identity on the AT Protocol", + }, }; function texName(base, lang) { diff --git a/system/public/papers.aesthetic.computer/index.html b/system/public/papers.aesthetic.computer/index.html --- a/system/public/papers.aesthetic.computer/index.html +++ b/system/public/papers.aesthetic.computer/index.html @@ -485,6 +485,12 @@
03/21r1Mar 21 06:24
+
+ +
From Auth0 to Decentralized Sign-In · ATProto OAuth, DIDs, Handle Verification
+
03/23r1Mar 23
+
+
diff --git a/system/public/papers.aesthetic.computer/platter.html b/system/public/papers.aesthetic.computer/platter.html --- a/system/public/papers.aesthetic.computer/platter.html +++ b/system/public/papers.aesthetic.computer/platter.html @@ -276,7 +276,7 @@

Research Platter

Full knowledge base for Aesthetic Computer papers and research.
- 359 pieces · 76 lib modules · 92 functions · 156 plans · 95 reports · 8 studies · 30+ readings · 20 papers + 359 pieces · 76 lib modules · 94 functions · 159 plans · 100 reports · 8 studies · 30+ readings · 26 papers
@@ -293,7 +293,7 @@

Papers

- 20 publications + 26 publications
Aesthetic Computer '26 — A Mobile-First Runtime for Creative Computing (arXiv, 5pp) @@ -439,11 +439,11 @@