SkateSpots/src/lib/crewAgent.ts
shakespeare.diy cdf8cee3a6 Update Sasquatch avatar to the correct mascot image
Point CREW_AGENT_AVATAR at the intended Sasquatch artwork. Since it's the
single source of truth, this updates the agent's image everywhere (panel
header/rows/empty state, crew page button, CrewForm toggle) and in the
agent's published kind-0 profile.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 18:59:05 +07:00

103 lines
4.1 KiB
TypeScript

/**
* Crew Agent identity (Phase 0)
* ─────────────────────────────
* The Crew Agent ("Sasquatch") is a Nostr identity that belongs to a crew.
* Its key is DETERMINISTICALLY DERIVED from the crew's addressable coordinate,
* so any client can reproduce the same agent identity for a given crew without
* storing a key anywhere.
*
* ⚠️ SECURITY TRADEOFF (documented, deliberate for v1):
* Because the key is derived from public data (the crew coordinate) plus a
* fixed, public salt in this source, ANYONE who reads this code can derive the
* same private key. This is acceptable for v1 because the agent only ever:
* - publishes its own kind-0 profile, and
* - (later, Phase 3) posts helper replies into its own crew's chat.
* It holds no funds and speaks only for itself. It is NOT a secure identity and
* must never be trusted for signing anything sensitive. A future version can
* swap this for a founder-controlled stored key without changing call sites.
*/
import { getPublicKey, finalizeEvent, nip19, type EventTemplate } from 'nostr-tools';
import { sha256 } from '@noble/hashes/sha2.js';
import { bytesToHex } from '@noble/hashes/utils.js';
import { SKATE_CREW_KIND } from '@/lib/types';
/** Public, fixed salt — versioned so we can rotate the scheme later if needed. */
const AGENT_KEY_SALT = 'skatespots:crew-agent:v1';
export interface CrewAgentIdentity {
/** 32-byte private key (hex). */
secretKeyHex: string;
/** 32-byte private key (bytes) — for signing. */
secretKey: Uint8Array;
/** Public key (hex). */
pubkey: string;
/** npub encoding of the public key. */
npub: string;
}
/**
* Build the crew's addressable coordinate `kind:pubkey:identifier`.
* This is the stable, public seed for the agent identity.
*/
export function crewCoord(crewPubkey: string, identifier: string): string {
return `${SKATE_CREW_KIND}:${crewPubkey}:${identifier}`;
}
/**
* Deterministically derive the crew agent's keypair from the crew coordinate.
* Same crew → same agent identity, everywhere, with no stored state.
*/
export function deriveCrewAgentIdentity(crewPubkey: string, identifier: string): CrewAgentIdentity {
const coord = crewCoord(crewPubkey, identifier);
// 32-byte secret = sha256(salt | coord). secp256k1 accepts any 32-byte scalar
// in range; sha256 output is overwhelmingly within range, and nostr-tools /
// noble will reject the astronomically unlikely invalid case at sign time.
const secretKey = sha256(new TextEncoder().encode(`${AGENT_KEY_SALT}|${coord}`));
const secretKeyHex = bytesToHex(secretKey);
const pubkey = getPublicKey(secretKey);
const npub = nip19.npubEncode(pubkey);
return { secretKeyHex, secretKey, pubkey, npub };
}
/** Just the agent pubkey — cheap when you don't need to sign. */
export function crewAgentPubkey(crewPubkey: string, identifier: string): string {
return deriveCrewAgentIdentity(crewPubkey, identifier).pubkey;
}
/** Sasquatch mascot avatar used for the agent's profile. */
export const CREW_AGENT_AVATAR =
'https://blossom.ditto.pub/fa182be220e42e2651d449ff2e38708ac2bc62c971ff7a64d29be776800b9a61.png';
/**
* Build (unsigned) the agent's kind-0 profile metadata for a crew.
* `bot: true` follows NIP-24 so clients can honestly label it as automated.
*/
export function buildCrewAgentProfile(crewName: string): EventTemplate {
const metadata = {
name: `Sasquatch · ${crewName}`,
display_name: `Sasquatch · ${crewName}`,
about: `The AI crew guide for ${crewName} on SkateSpots. Ask me about spots, events, coaches, gear and more — I answer with live data. 🛹`,
picture: CREW_AGENT_AVATAR,
bot: true,
};
return {
kind: 0,
content: JSON.stringify(metadata),
tags: [],
created_at: Math.floor(Date.now() / 1000),
};
}
/**
* Sign an event template AS the crew agent (using its derived key).
* Used to publish the agent's profile now, and its chat replies in Phase 3.
*/
export function signAsCrewAgent(
crewPubkey: string,
identifier: string,
template: EventTemplate,
) {
const { secretKey } = deriveCrewAgentIdentity(crewPubkey, identifier);
return finalizeEvent(template, secretKey);
}