SkateSpots/src/lib/proAuth.ts
SkateSpots ccc04cc172 feat(sasquatch-pro): server-side Pro gate on the AI proxy + client credentials
- netlify/functions/ai.mjs: optional Pro enforcement (SASQUATCH_REQUIRE_PRO=1).
  Requires a NIP-98 kind-27235 auth header (x-sasquatch-auth) and, unless the
  caller is a platform owner, an active Pro pass (x-sasquatch-pro = a
  provider-authored zap receipt) verified OFFLINE — signature, user binding,
  platform tag, and paid-amount → tier. Off by default.
- src/lib/proAuth.ts: signs the NIP-98 once per session (no Amber popup per
  message) and attaches the Pro receipt.
- sasquatchBrain: threads auth headers through the proxy (streaming + non).
- useSasquatchPro: exposes the winning receipt event.
- Verified: 200 with the gate off, 401 with it on (env reset afterwards).
2026-09-27 14:18:33 +07:00

55 lines
1.7 KiB
TypeScript

/**
* Pro auth for the AI proxy.
*
* When the proxy's Pro gate is enabled (`SASQUATCH_REQUIRE_PRO=1`) every
* `/api/ai` request must carry:
* • `x-sasquatch-auth` — a base64 NIP-98 (kind 27235) event proving the caller;
* • `x-sasquatch-pro` — base64 of the Pro zap receipt proving an active pass.
*
* The NIP-98 event is signed ONCE per session and reused, so an interactive
* signer (Amber) doesn't pop on every message.
*/
import type { NostrEvent } from '@nostrify/nostrify';
type SignEvent = (event: {
kind: number;
created_at: number;
tags: string[][];
content: string;
}) => Promise<NostrEvent>;
let cachedAuthHeader: string | null = null;
let cachedAuthPubkey: string | null = null;
function toBase64(value: string): string {
// Event JSON is ASCII (hex pubkeys/ids, tags), so btoa is safe here.
return btoa(value);
}
export async function buildProAuthHeaders(opts: {
pubkey?: string;
signEvent?: SignEvent;
receiptEvent?: NostrEvent | null;
}): Promise<Record<string, string>> {
const { pubkey, signEvent, receiptEvent } = opts;
const headers: Record<string, string> = {};
if (!pubkey || !signEvent) return headers;
if (cachedAuthPubkey !== pubkey || !cachedAuthHeader) {
const event = await signEvent({
kind: 27235,
created_at: Math.floor(Date.now() / 1000),
tags: [
['u', 'https://skate-spots.org/api/ai'],
['method', 'POST'],
],
content: '',
});
cachedAuthHeader = toBase64(JSON.stringify(event));
cachedAuthPubkey = pubkey;
}
headers['x-sasquatch-auth'] = cachedAuthHeader;
if (receiptEvent) headers['x-sasquatch-pro'] = toBase64(JSON.stringify(receiptEvent));
return headers;
}