1<!doctype html>2<html lang="en">3 <head>4 <meta charset="utf-8" />5 <meta name="viewport" content="width=device-width, initial-scale=1" />6 <title>Peach Passkey Smoke</title>7 <style>8 :root {9 color-scheme: dark;10 font-family: ui-sans-serif, system-ui, sans-serif;11 }1213 body {14 margin: 0;15 min-height: 100vh;16 display: grid;17 place-items: center;18 background:19 radial-gradient(circle at top, rgba(217, 104, 48, 0.2), transparent 30%),20 #111009;21 color: #f0e8d8;22 }2324 main {25 width: min(560px, calc(100vw - 40px));26 padding: 28px;27 border-radius: 18px;28 background: rgba(255, 255, 255, 0.03);29 border: 1px solid rgba(240, 232, 216, 0.08);30 box-shadow: 0 20px 70px rgba(0, 0, 0, 0.35);31 }3233 h1 {34 margin: 0 0 8px;35 font-size: 28px;36 }3738 p {39 margin: 0 0 16px;40 color: rgba(240, 232, 216, 0.72);41 }4243 .actions {44 display: flex;45 gap: 12px;46 margin-bottom: 18px;47 }4849 button {50 border: 0;51 border-radius: 999px;52 padding: 11px 16px;53 font: inherit;54 cursor: pointer;55 color: #111009;56 background: #f0e8d8;57 }5859 button.primary {60 background: #d96830;61 color: #fff7ed;62 }6364 .status {65 font-family: ui-monospace, SFMono-Regular, monospace;66 font-size: 13px;67 padding: 12px 14px;68 border-radius: 12px;69 background: rgba(255, 255, 255, 0.04);70 white-space: pre-wrap;71 }7273 .status[data-state="ok"] {74 outline: 1px solid rgba(94, 229, 94, 0.35);75 }7677 .status[data-state="error"] {78 outline: 1px solid rgba(229, 85, 85, 0.35);79 }80 </style>81 </head>82 <body>83 <main>84 <h1>Peach Passkey Smoke</h1>85 <p>Exercises WebAuthn registration and authentication against the local Peach extension provider.</p>86 <div class="actions">87 <button id="create" class="primary">Create passkey</button>88 <button id="get">Authenticate</button>89 </div>90 <div id="create-status" class="status" data-state="idle">Create idle</div>91 <div id="get-status" class="status" data-state="idle" style="margin-top: 12px">Get idle</div>92 </main>9394 <script>95 const state = {96 credentialJson: null,97 };9899 function bytes(...values) {100 return Uint8Array.from(values);101 }102103 function base64urlToBytes(input) {104 const padded = input.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(input.length / 4) * 4, "=");105 const binary = atob(padded);106 const out = new Uint8Array(binary.length);107 for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);108 return out;109 }110111 function setStatus(id, stateName, value) {112 const el = document.getElementById(id);113 el.dataset.state = stateName;114 el.textContent = value;115 }116117 async function createPasskey() {118 setStatus("create-status", "pending", "Creating passkey...");119 try {120 const credential = await navigator.credentials.create({121 publicKey: {122 challenge: bytes(1, 2, 3, 4, 5, 6, 7, 8),123 rp: { id: location.hostname, name: "Peach Passkey Smoke" },124 user: {125 id: bytes(10, 20, 30, 40, 50, 60, 70, 80),126 name: "[email protected]",127 displayName: "June Local",128 },129 pubKeyCredParams: [{ type: "public-key", alg: -7 }],130 timeout: 60_000,131 attestation: "none",132 authenticatorSelection: {133 residentKey: "required",134 userVerification: "required",135 },136 },137 });138139 state.credentialJson = credential.toJSON();140 setStatus(141 "create-status",142 "ok",143 `Created ${state.credentialJson.id}\n${JSON.stringify(state.credentialJson, null, 2)}`144 );145 } catch (error) {146 setStatus("create-status", "error", `Create failed: ${error.name}: ${error.message}`);147 }148 }149150 async function authenticate() {151 if (!state.credentialJson) {152 setStatus("get-status", "error", "Authenticate failed: missing created credential");153 return;154 }155156 setStatus("get-status", "pending", "Authenticating...");157 try {158 const assertion = await navigator.credentials.get({159 publicKey: {160 challenge: bytes(8, 7, 6, 5, 4, 3, 2, 1),161 rpId: location.hostname,162 allowCredentials: [163 {164 type: "public-key",165 id: base64urlToBytes(state.credentialJson.rawId),166 },167 ],168 userVerification: "required",169 timeout: 60_000,170 },171 });172173 const assertionJson = assertion.toJSON();174 setStatus(175 "get-status",176 "ok",177 `Authenticated ${assertionJson.id}\n${JSON.stringify(assertionJson, null, 2)}`178 );179 } catch (error) {180 setStatus("get-status", "error", `Get failed: ${error.name}: ${error.message}`);181 }182 }183184 document.getElementById("create").addEventListener("click", () => {185 void createPasskey();186 });187 document.getElementById("get").addEventListener("click", () => {188 void authenticate();189 });190 </script>191 </body>192</html>