- 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).
55 lines
1.7 KiB
TypeScript
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;
|
|
}
|