Skip to content

[SDK] Add JWKS generation script #63

@bitbeckers

Description

@bitbeckers

Describe the feature you'd like to request

For local testing oauth, users need jwks public/private pairs. Let help them get those

Describe the solution you'd like

Add a script, maybe executable with npx @hypercerts-org/sdk-core generate-jwks

maybe something like this

#!/usr/bin/env tsx
/**
 * Generate JWK keypair for ATProto OAuth
 *
 * This script generates an ES256 (P-256) keypair for use with ATProto OAuth.
 * - Private keyset: Output to console for copying to .env
 * - Public keyset: Written to public/jwks.json for serving via endpoint
 *
 * Usage:
 *   pnpm generate:jwks
 */

import { generateKeyPair, exportJWK } from "jose";
import { writeFileSync } from "fs";
import { join } from "path";
import { randomUUID } from "crypto";

async function generateJWKs() {
  console.log("🔐 Generating ES256 (P-256) keypair for ATProto OAuth...\n");

  // Generate ES256 keypair
  const { publicKey, privateKey } = await generateKeyPair("ES256", {
    extractable: true,
  });

  // Generate a unique key ID
  const kid = randomUUID();

  // Export keys to JWK format
  const publicJWK = await exportJWK(publicKey);
  const privateJWK = await exportJWK(privateKey);

  // Add required fields
  publicJWK.kid = kid;
  publicJWK.alg = "ES256";
  publicJWK.use = "sig";

  privateJWK.kid = kid;
  privateJWK.alg = "ES256";
  privateJWK.use = "sig";

  // Create keysets
  const privateKeyset = {
    keys: [privateJWK],
  };

  const publicKeyset = {
    keys: [publicJWK],
  };

  // Write public keyset to file
  const publicPath = join(process.cwd(), "public", "jwks.json");
  writeFileSync(publicPath, JSON.stringify(publicKeyset, null, 2));
  console.log(`✅ Public keyset written to: ${publicPath}\n`);

  // Output private keyset for .env
  console.log("📋 Copy this PRIVATE keyset to your .env.local file:");
  console.log(
    "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
  );
  console.log(`ATPROTO_JWK_PRIVATE='${JSON.stringify(privateKeyset)}'`);
  console.log(
    "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n",
  );

  console.log("⚠️  IMPORTANT:");
  console.log("  1. Add the ATPROTO_JWK_PRIVATE to your .env.local file");
  console.log("  2. NEVER commit the private keyset to version control");
  console.log("  3. The public keyset (public/jwks.json) is safe to commit");
  console.log(
    "  4. Generate new keys for each environment (dev/staging/prod)\n",
  );

  console.log("📝 Also add this to your .env.local:");
  console.log(
    "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
  );
  console.log('NEXT_PUBLIC_ATPROTO_JWKS_URI="http://localhost:3000/jwks.json"');
  console.log(
    "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n",
  );

  console.log("✨ JWK generation complete!\n");
}

generateJWKs().catch((error) => {
  console.error("❌ Error generating JWKs:", error);
  process.exit(1);
});

Describe alternatives you've considered

.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions