Files
botfights/.claude/skills/nostr-auth/nostr-auth.md
T
DorianandClaude Opus 4.6 47d20fbe66 feat: v2 — queue matchmaking, procedural audio, sprite archetypes, auth
- Add queue-based matchmaking with Elo-proximity and 10s timeout
- Procedural sound engine (SFX, voice announcer, 4-track music)
- Sprite system refactored into 6 archetypes (standard, lobster, sheep, cyborg, blob, tank)
- 42+ fight choreographies with themed/generic/wild card selection
- 4 KO finish styles, super-speed mode, hyperdetail close-ups
- Auth routes, JoinBout page, bot profile with stats
- 7-tier ranking system (Baby through Legend)
- Arena and challenge system expansions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 22:13:19 +00:00

3.7 KiB

description, match
description match
Nostr keypair authentication for the Syntropy Institute portal (auth only, no relays)
nostr
auth
login
keypair
sign
challenge

Nostr Auth Skill

When to Use

When working with authentication flows, keypair management, or NIP-98 API auth in the portal.

Key Principle

Nostr is used ONLY for cryptographic authentication. No relay connections. No event publishing. Just keypairs and signatures.

Keypair Generation (Easy Mode)

import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';

// Generate new identity
const secretKey = generateSecretKey(); // Uint8Array
const publicKey = getPublicKey(secretKey); // hex string

// Encode for display (only when user requests it)
const npub = nip19.npubEncode(publicKey);
const nsec = nip19.nsecEncode(secretKey);

Private Key Encryption (for localStorage)

// Encrypt private key with user's passphrase before storing
async function encryptPrivateKey(secretKey: Uint8Array, passphrase: string): Promise<string> {
  const encoder = new TextEncoder();
  const keyMaterial = await crypto.subtle.importKey(
    'raw', encoder.encode(passphrase), 'PBKDF2', false, ['deriveKey']
  );
  const salt = crypto.getRandomValues(new Uint8Array(16));
  const derivedKey = await crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt, iterations: 600000, hash: 'SHA-256' },
    keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['encrypt']
  );
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encrypted = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv }, derivedKey, secretKey
  );
  // Return salt + iv + ciphertext as base64
  const combined = new Uint8Array([...salt, ...iv, ...new Uint8Array(encrypted)]);
  return btoa(String.fromCharCode(...combined));
}

NIP-07 Detection (Nostr Native)

// Check for browser extension
function hasNostrExtension(): boolean {
  return typeof window !== 'undefined' && 'nostr' in window;
}

// Sign with extension
async function signWithExtension(event: object): Promise<object> {
  return await (window as any).nostr.signEvent(event);
}

// Get public key from extension
async function getExtensionPubkey(): Promise<string> {
  return await (window as any).nostr.getPublicKey();
}

Challenge-Response Auth (NIP-98 style)

import { finalizeEvent, verifyEvent } from 'nostr-tools';

// Client: Sign auth challenge
function createAuthEvent(secretKey: Uint8Array, url: string, method: string) {
  const event = finalizeEvent({
    kind: 27235, // NIP-98 HTTP Auth
    created_at: Math.floor(Date.now() / 1000),
    tags: [
      ['u', url],
      ['method', method],
    ],
    content: '',
  }, secretKey);
  return event;
}

// Send as Authorization header:
// Authorization: Nostr <base64-encoded-event-json>

// Server: Verify auth event
function verifyAuthEvent(event: any, expectedUrl: string, expectedMethod: string): boolean {
  if (!verifyEvent(event)) return false;
  if (event.kind !== 27235) return false;
  const urlTag = event.tags.find((t: string[]) => t[0] === 'u');
  const methodTag = event.tags.find((t: string[]) => t[0] === 'method');
  if (urlTag?.[1] !== expectedUrl) return false;
  if (methodTag?.[1] !== expectedMethod) return false;
  // Check timestamp is within 60 seconds
  if (Math.abs(Date.now() / 1000 - event.created_at) > 60) return false;
  return true;
}

Rules

  • NEVER store private keys on the server
  • NEVER connect to any Nostr relay
  • NEVER transmit private keys over the network
  • ALWAYS encrypt private keys before storing in localStorage
  • Use PBKDF2 with at least 600,000 iterations for key derivation
  • Auth events expire after 60 seconds
  • Server only stores npub (public key)