Packages
atproto_browser
0.1.0
A browser-native atproto OAuth + XRPC client for Gleam: WebCrypto DPoP, PKCE, and async transport, for building a public-client SPA with no server-side session custody.
Current section
Files
Jump to
Current section
Files
src/atproto/browser/dpop_ffi.mjs
// WebCrypto-backed DPoP key generation + ES256 signing (RFC 9449). Requires
// a secure context (https, or localhost) for `crypto.subtle` to exist at
// all — not an extra constraint in practice, since DPoP-bound OAuth already
// requires one.
import { Result$Ok, Result$Error } from "../../gleam.mjs";
const KEY_ALG = { name: "ECDSA", namedCurve: "P-256" };
const SIGN_ALG = { name: "ECDSA", hash: "SHA-256" };
export function generateKey() {
// Non-extractable: the raw private scalar can never leave the WebCrypto
// boundary, even if this page is compromised by XSS — every proof still
// has to be signed live, through this same API.
return crypto.subtle.generateKey(KEY_ALG, false, ["sign", "verify"]);
}
function base64url(bytes) {
const view = new Uint8Array(bytes);
let binary = "";
for (let i = 0; i < view.length; i++) {
binary += String.fromCharCode(view[i]);
}
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
export async function publicJwkCoords(keyPair) {
try {
const jwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
return Result$Ok([jwk.x, jwk.y]);
} catch (err) {
return Result$Error(String(err));
}
}
export async function signEs256(keyPair, signingInput) {
try {
const data = new TextEncoder().encode(signingInput);
const signature = await crypto.subtle.sign(
SIGN_ALG,
keyPair.privateKey,
data,
);
// WebCrypto's ECDSA signature is already raw R‖S (IEEE P1363) — no
// DER-to-R‖S conversion needed, unlike the Node/kryptos path.
return Result$Ok(base64url(signature));
} catch (err) {
return Result$Error(String(err));
}
}