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>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
335c148866
commit
47d20fbe66
@@ -0,0 +1,116 @@
|
||||
---
|
||||
description: Nostr keypair authentication for the Syntropy Institute portal (auth only, no relays)
|
||||
match:
|
||||
- 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)
|
||||
```typescript
|
||||
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)
|
||||
```typescript
|
||||
// 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)
|
||||
```typescript
|
||||
// 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)
|
||||
```typescript
|
||||
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)
|
||||
Reference in New Issue
Block a user