atproto pds in zig pds.zat.dev
pds atproto
Something went wrong. Try again.
Zig
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137const std = @import("std");const config = @import("../core/config.zig");const http_api = @import("../http/api.zig");
const http = std.http;
pub fn page(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const body = try std.mem.concat(arena.allocator(), u8, &.{ html_before_invite_cta, try inviteCta(arena.allocator()), html_after_invite_cta, }); try http_api.respond(request, .ok, body, &html_headers);}
fn inviteCta(allocator: std.mem.Allocator) ![]const u8 { const operator = config.operatorHandle(); if (operator.len == 0) return ""; const escaped = try escapeHtml(allocator, operator); return std.fmt.allocPrint( allocator, "<p class=\"hint\">Need an invite? DM <a href=\"https://bsky.app/profile/{s}\" rel=\"noopener\">@{s}</a>, the operator of this PDS.</p>", .{ escaped, escaped }, );}
fn escapeHtml(allocator: std.mem.Allocator, text: []const u8) ![]const u8 { var out: std.Io.Writer.Allocating = .init(allocator); defer out.deinit(); for (text) |c| switch (c) { '&' => try out.writer.writeAll("&"), '<' => try out.writer.writeAll("<"), '>' => try out.writer.writeAll(">"), '"' => try out.writer.writeAll("""), '\'' => try out.writer.writeAll("'"), else => try out.writer.writeByte(c), }; return out.toOwnedSlice();}
const html_headers = [_]http.Header{ .{ .name = "content-type", .value = "text/html; charset=utf-8" }, .{ .name = "cache-control", .value = "no-store" }, .{ .name = "access-control-allow-origin", .value = "*" }, .{ .name = "access-control-allow-private-network", .value = "true" }, .{ .name = "connection", .value = "close" },};
const html_before_invite_cta = \\<!doctype html> \\<html lang="en"> \\<head> \\<meta charset="utf-8"> \\<meta name="viewport" content="width=device-width, initial-scale=1"> \\<title>join zds</title> \\<meta name="description" content="Create an account on this ZDS PDS."> \\<link rel="icon" href="/favicon.svg" type="image/svg+xml"> \\<style> \\:root{color-scheme:dark;--bg:#070807;--panel:#101510;--line:#5b7159;--text:#f1f2ec;--muted:#c2bcaf;--accent:#8fb0ff;--accent-text:#061021;--green:#37c978;--field:#101410;--bad:#ffb4a8;--warn:#f4d35e;--focus:#f4d35e} \\@media (prefers-color-scheme:light){:root{--bg:#f6f4ed;--panel:#fffdf7;--line:#847866;--text:#161412;--muted:#4f493f;--accent:#1d4ed8;--accent-text:#fffdf7;--green:#17683a;--field:#fffaf1;--bad:#9f1d1d;--warn:#735f00;--focus:#5a3fc0;color-scheme:light}} \\*{box-sizing:border-box}body{margin:0;min-height:100vh;background:radial-gradient(circle at 18% 0,color-mix(in srgb,var(--green) 18%,transparent),transparent 34%),var(--bg);color:var(--text);font:15px/1.55 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace} \\.shell{width:min(100%,640px);margin:0 auto;padding:28px 16px 42px}a{color:var(--accent)}.brand{display:inline-flex;margin-bottom:42px;text-decoration:none;font-weight:800;color:var(--text)}h1{margin:0;font-size:clamp(52px,15vw,112px);line-height:.88}p{color:var(--muted);margin:12px 0 0;max-width:68ch}strong{color:var(--text)}code{overflow-wrap:anywhere;color:var(--accent)} \\.panel{margin-top:28px;border:1px solid var(--line);border-radius:10px;background:linear-gradient(135deg,color-mix(in srgb,var(--green) 10%,transparent),transparent 46%),linear-gradient(315deg,color-mix(in srgb,var(--accent) 8%,transparent),transparent 44%),var(--panel);padding:18px} \\label{display:block;margin:14px 0 6px;font-weight:760}input,select{width:100%;font:inherit;color:var(--text);background:var(--field);border:1px solid var(--line);border-radius:9px;padding:12px}input:focus-visible,select:focus-visible,button:focus-visible{outline:3px solid var(--focus);outline-offset:2px}button{width:100%;margin-top:22px;border:1px solid var(--accent);border-radius:10px;background:var(--accent);color:var(--accent-text);font:inherit;font-weight:800;padding:12px;cursor:pointer}button:disabled{opacity:.62;cursor:not-allowed} \\.status{min-height:1.5em;color:var(--muted)}.error{color:var(--bad)}.ok{color:var(--green)}.hint{font-size:.92rem;color:var(--muted);margin:8px 0 0} \\.handle-row{display:grid;grid-template-columns:1fr auto;align-items:stretch}.handle-row input{border-radius:9px 0 0 9px;border-right:0}.handle-suffix{display:flex;align-items:center;border:1px solid var(--line);border-radius:0 9px 9px 0;background:color-mix(in srgb,var(--field) 60%,transparent);color:var(--muted);padding:0 12px;white-space:nowrap}select.handle-suffix{width:auto;border-radius:0 9px 9px 0;color:var(--text)} \\.hidden{display:none} \\</style> \\</head> \\<body> \\<main class="shell"> \\<a class="brand" href="/">zds</a> \\<h1>join</h1> \\<p>Create a new account on <strong id="host">this PDS</strong>.</p> \\<section class="panel"> \\<p style="margin-top:0">This creates a <strong>brand new account</strong>. Already have an atproto account you want to bring here? Use a migration tool such as <a href="https://pdsmoover.com" rel="noopener">PDS Moover</a> instead — migration is a different flow with different identity semantics.</p> \\</section> \\<section class="panel"> \\<form id="signup-form"> \\<label for="handle-name">handle</label> \\<div class="handle-row"> \\<input id="handle-name" name="handle" autocomplete="username" spellcheck="false" required> \\<span id="suffix-static" class="handle-suffix">…</span> \\<select id="suffix-select" class="handle-suffix hidden" aria-label="handle suffix"></select> \\</div> \\<p class="hint" id="handle-preview">you'll be <strong>…</strong></p> \\<label for="email">email</label> \\<input id="email" name="email" type="email" autocomplete="email" required> \\<label for="password">password</label> \\<input id="password" name="password" type="password" autocomplete="new-password" minlength="8" required> \\<label for="confirm">confirm password</label> \\<input id="confirm" name="confirm" type="password" autocomplete="new-password" required> \\<div id="invite-field" class="hidden"> \\<label for="invite">invite code</label> \\<input id="invite" name="invite" autocomplete="off" spellcheck="false"> \\<p class="hint">This server requires an invite. If someone sent you a signup link, the code is already filled in.</p>;
const html_after_invite_cta = \\</div> \\<p class="hint">This is an experimental personal data server. Your account and repo live here; you can migrate them elsewhere later.</p> \\<button id="create">create account</button> \\</form> \\<p id="status" class="status" aria-live="polite"></p> \\</section> \\<p class="hint">Already have an account here? <a href="/account">Sign in.</a></p> \\</main> \\<script> \\const $=(id)=>document.getElementById(id); \\const status=$('status'),form=$('signup-form'),nameInput=$('handle-name'),suffixStatic=$('suffix-static'),suffixSelect=$('suffix-select'),preview=$('handle-preview'); \\let suffixes=[]; \\const setStatus=(text,cls='')=>{status.className='status '+cls;status.textContent=text}; \\const suffix=()=>suffixes.length>1?suffixSelect.value:(suffixes[0]||''); \\const fullHandle=()=>nameInput.value.trim().toLowerCase()+suffix(); \\function updatePreview(){const name=nameInput.value.trim().toLowerCase();preview.innerHTML='';preview.append("you'll be ");const b=document.createElement('strong');b.textContent=name?fullHandle():'…';preview.append(b)} \\nameInput.addEventListener('input',updatePreview);suffixSelect.addEventListener('change',updatePreview); \\async function describe(){const r=await fetch('/xrpc/com.atproto.server.describeServer');if(!r.ok)throw new Error('could not load server description');const d=await r.json();$('host').textContent=location.host;suffixes=(d.availableUserDomains||[]).filter(s=>typeof s==='string'&&s.startsWith('.'));if(!suffixes.length){form.querySelectorAll('input,select,button').forEach(el=>el.disabled=true);setStatus('this server does not advertise any hosted handle domains, so accounts cannot be created here','error');return} \\if(suffixes.length>1){suffixStatic.classList.add('hidden');suffixSelect.classList.remove('hidden');for(const s of suffixes){const o=document.createElement('option');o.value=s;o.textContent=s;suffixSelect.append(o)}}else{suffixStatic.textContent=suffixes[0]} \\if(d.inviteCodeRequired){$('invite-field').classList.remove('hidden');$('invite').required=true;const code=new URLSearchParams(location.search).get('code');if(code)$('invite').value=code} \\updatePreview()} \\describe().catch(err=>setStatus(err.message,'error')); \\form.addEventListener('submit',async(e)=>{e.preventDefault();try{const name=nameInput.value.trim().toLowerCase();if(!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(name))throw new Error('handle may only use letters, numbers, and inner hyphens');if($('password').value!==$('confirm').value)throw new Error('passwords do not match'); \\const body={handle:fullHandle(),email:$('email').value.trim(),password:$('password').value};if(!$('invite-field').classList.contains('hidden'))body.inviteCode=$('invite').value.trim(); \\$('create').disabled=true;setStatus('creating account...'); \\const r=await fetch('/xrpc/com.atproto.server.createAccount',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)}); \\const text=await r.text();let out={};try{out=text?JSON.parse(text):{}}catch{} \\if(!r.ok){const code=out.error?out.error+': ':'';throw new Error(code+(out.message||'account creation failed'))} \\setStatus('account created — welcome, '+out.handle,'ok'); \\sessionStorage.setItem('zds-signup-session',JSON.stringify({accessJwt:out.accessJwt,refreshJwt:out.refreshJwt,handle:out.handle,did:out.did})); \\location.assign('/account')}catch(err){setStatus(err.message||String(err),'error')}finally{$('create').disabled=false}}); \\</script> \\</body> \\</html>;