getKeyset
@atiproto/edge-oauth-client
Builds a Keyset from a private JWK string and caches the resulting Promise in module scope. Pass the result as the keyset option when constructing a confidential EdgeOAuthClient.
Signature
function getKeyset(jwk: string | undefined): Promise<Keyset | null>
Accepts the raw JSON string of a single private JWK (typically read from an environment variable). Returns null when the input is empty or undefined, which is the correct shape for a public client.
Example
import { EdgeOAuthClient, getKeyset, patchGlobalRequestObject } from "@atiproto/edge-oauth-client";import { KvStateStore, KvSessionStore } from "@atiproto/kv-oauth-state-store";patchGlobalRequestObject();interface Env {OAUTH_STATE_KV: KVNamespace;OAUTH_SESSION_KV: KVNamespace;OAUTH_PRIVATE_JWK?: string;}async function createOAuthClient(env: Env) {return new EdgeOAuthClient({clientMetadata: {client_id: "https://your-app.example.com/oauth/client-metadata.json",// ... other metadata, with// token_endpoint_auth_method: "private_key_jwt"// token_endpoint_auth_signing_alg: "ES256"},keyset: (await getKeyset(env.OAUTH_PRIVATE_JWK)) ?? undefined,stateStore: new KvStateStore(env.OAUTH_STATE_KV),sessionStore: new KvSessionStore(env.OAUTH_SESSION_KV),});}
Caching Behavior
The first call memoizes the Promise in a module-level variable; every subsequent call returns the same Promise without re-parsing the JWK. This matches the lifetime of a Worker isolate, which should only ever serve one client identity. Because the cache is keyed on module identity rather than input, the argument passed to later calls is ignored.
Generating a Key
The JWK is a private key that signs client assertions at the token endpoint. Treat it like any other secret: keep it out of source control, store it in Wrangler secrets (wrangler secret put OAUTH_PRIVATE_JWK), and rotate it only when necessary — rotating invalidates every currently-issued session because refresh tokens can no longer be signed.
The following script prints a fresh ES256 private JWK on stdout. Pipe it directly into wrangler secret put to avoid writing the key to disk:
node scripts/generate-jwk.mjs | wrangler secret put OAUTH_PRIVATE_JWK
scripts/generate-jwk.mjs:
#!/usr/bin/env node// Generates an ES256 private JWK suitable for OAUTH_PRIVATE_JWK.import { subtle, randomUUID } from "node:crypto";export async function generateJwk() {const { privateKey } = await subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" },true,["sign", "verify"],);const jwk = await subtle.exportKey("jwk", privateKey);jwk.alg = "ES256";jwk.key_ops = ["sign"];jwk.kid = randomUUID();return jwk;}if (import.meta.url === `file://${process.argv[1]}`) {process.stdout.write(JSON.stringify(await generateJwk()) + "\n");}
The exported generateJwk() function can also be imported from tests or setup scripts when a fresh key is needed programmatically.