diff --git a/README.md b/README.md index 8db0bfa..4c02f40 100644 --- a/README.md +++ b/README.md @@ -6,33 +6,35 @@ A template for creating custom feed generators for [sprk.so](https://sprk.so). The feed generator has three main components: -1. **Ingester** (`ingester/`) - Consumes the Spark firehose in real-time and - indexes records to MongoDB. By default, it indexes `so.sprk.feed.post` - records. Handlers for `follow`, `like`, and `repost` are included but +1. **Ingester** (`ingester/`) - Consumes the Spark firehose in real-time and + indexes records to MongoDB. By default, it indexes `so.sprk.feed.post` + records. Handlers for `follow`, `like`, and `repost` are included but disabled - enable them by uncommenting in `ingester/index.ts`. -2. **Algorithms** (`algos/`) - Define how posts are selected and sorted for - your feed. Each algorithm exports: +2. **Algorithms** (`algos/`) - Define how posts are selected and sorted for your + feed. Each algorithm exports: - `handler` - Query function that returns posts from the database - `needsAuth` - Whether the feed requires user authentication - `publisherDid` - The DID of the feed publisher - `rkey` - Unique identifier for this algorithm -3. **API Server** - Exposes XRPC endpoints that Spark clients call to fetch - feed content (`so.sprk.feed.getFeedSkeleton`, `so.sprk.feed.describeFeedGenerator`). - You won't need to modify these when creating a feed. +3. **API Server** - Exposes XRPC endpoints that Spark clients call to fetch feed + content (`so.sprk.feed.getFeedSkeleton`, + `so.sprk.feed.describeFeedGenerator`). You won't need to modify these when + creating a feed. ## Creating Custom Feeds -**For topic/community feeds:** Filter posts at the ingester level. Modify -the handler in `ingester/handlers/post.ts` to only index posts matching your +**For topic/community feeds:** Filter posts at the ingester level. Modify the +handler in `ingester/handlers/post.ts` to only index posts matching your criteria (hashtags, keywords, specific authors, etc.). -**For personalized/sorted feeds:** Create a new algorithm in `algos/`. Copy -`simple-desc.ts` as a starting point, then modify the query logic. Register -your algorithm in `algos/index.ts`. +**For personalized/sorted feeds:** Create a new algorithm in `algos/`. Copy +`simple-desc.ts` as a starting point, then modify the query logic. Register your +algorithm in `algos/index.ts`. Example algorithm structure: + ```ts // algos/my-feed.ts export const info = { @@ -40,9 +42,9 @@ export const info = { // Query posts from ctx.db.models.Post // Return { cursor, feed: [{ post: "at://..." }] } }, - needsAuth: false, // Set true if feed needs user's DID + needsAuth: false, // Set true if feed needs user's DID publisherDid: "did:plc:your-did", - rkey: "my-feed", // at://your-did/so.sprk.feed.generator/my-feed + rkey: "my-feed", // at://your-did/so.sprk.feed.generator/my-feed } as Algorithm; ``` @@ -97,10 +99,10 @@ The server will be available at `http://localhost:3000`. ## Endpoints -| Endpoint | Description | -|----------|-------------| -| `GET /` | Service info | -| `GET /health` | Health check | -| `GET /.well-known/did.json` | DID document for `did:web` resolution | -| `GET /xrpc/so.sprk.feed.describeFeedGenerator` | List available feeds | -| `GET /xrpc/so.sprk.feed.getFeedSkeleton` | Fetch feed posts | +| Endpoint | Description | +| ---------------------------------------------- | ------------------------------------- | +| `GET /` | Service info | +| `GET /health` | Health check | +| `GET /.well-known/did.json` | DID document for `did:web` resolution | +| `GET /xrpc/so.sprk.feed.describeFeedGenerator` | List available feeds | +| `GET /xrpc/so.sprk.feed.getFeedSkeleton` | Fetch feed posts | diff --git a/utils/auth.ts b/utils/auth.ts index 75a9868..7009ab5 100644 --- a/utils/auth.ts +++ b/utils/auth.ts @@ -1,4 +1,4 @@ -import { MethodAuthContext, parseReqNsid, verifyJwt } from "@atp/xrpc-server"; +import { MethodAuthContext, verifyJwt } from "@atp/xrpc-server"; import { DidResolver } from "@atp/identity"; export type NullOutput = { @@ -12,7 +12,6 @@ export type NullOutput = { export type StandardOutput = { credentials: { type: "standard"; - aud: string; iss: string; }; artifacts: unknown; @@ -25,31 +24,54 @@ export class AuthVerifier { this.ownDid = ownDid ?? ""; this.didResolver = didResolver; } + standardOptional = async ( ctx: MethodAuthContext, ): Promise => { - const authorization = ctx.req.headers.get("Authorization"); - if (authorization?.startsWith("Bearer ") ?? false) { - const jwt = authorization?.replace("Bearer ", "").trim(); - const nsid = parseReqNsid(ctx.req); - if (!jwt) return this.nullCreds(); - const parsed = await verifyJwt( - jwt, - this.ownDid, - nsid, - async (did: string) => { - return await this.didResolver.resolveAtprotoKey(did); - }, - ); - return { - credentials: { - type: "standard", - aud: parsed.aud, - iss: parsed.iss, - }, - artifacts: null, - }; - } else { + try { + const authorization = ctx.req.headers.get("Authorization") ?? ""; + + if (!authorization) { + return this.nullCreds(); + } + + // Check for Bearer token + const BEARER = "Bearer "; + if (authorization.startsWith(BEARER)) { + const jwt = authorization.replace(BEARER, "").trim(); + + try { + const parsed = await verifyJwt( + jwt, + null, + null, + async (did: string) => { + return await this.didResolver.resolveAtprotoKey(did); + }, + ); + return { + credentials: { + type: "standard", + iss: parsed.iss, + }, + artifacts: null, + }; + } catch (error) { + // Log JWT verification errors for debugging + console.error("JWT verification failed:", error); + console.error( + "JWT preview:", + jwt.length > 20 ? jwt.substring(0, 20) + "..." : jwt, + ); + console.error("ownDid:", this.ownDid); + // If JWT verification fails, treat as unauthenticated + return this.nullCreds(); + } + } else { + return this.nullCreds(); + } + } catch (error) { + console.error("Unexpected error in standardOptional:", error); return this.nullCreds(); } };