Spark feed generator template
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix auth

+70 -46
+24 -22
README.md
··· 6 6 7 7 The feed generator has three main components: 8 8 9 - 1. **Ingester** (`ingester/`) - Consumes the Spark firehose in real-time and 10 - indexes records to MongoDB. By default, it indexes `so.sprk.feed.post` 11 - records. Handlers for `follow`, `like`, and `repost` are included but 9 + 1. **Ingester** (`ingester/`) - Consumes the Spark firehose in real-time and 10 + indexes records to MongoDB. By default, it indexes `so.sprk.feed.post` 11 + records. Handlers for `follow`, `like`, and `repost` are included but 12 12 disabled - enable them by uncommenting in `ingester/index.ts`. 13 13 14 - 2. **Algorithms** (`algos/`) - Define how posts are selected and sorted for 15 - your feed. Each algorithm exports: 14 + 2. **Algorithms** (`algos/`) - Define how posts are selected and sorted for your 15 + feed. Each algorithm exports: 16 16 - `handler` - Query function that returns posts from the database 17 17 - `needsAuth` - Whether the feed requires user authentication 18 18 - `publisherDid` - The DID of the feed publisher 19 19 - `rkey` - Unique identifier for this algorithm 20 20 21 - 3. **API Server** - Exposes XRPC endpoints that Spark clients call to fetch 22 - feed content (`so.sprk.feed.getFeedSkeleton`, `so.sprk.feed.describeFeedGenerator`). 23 - You won't need to modify these when creating a feed. 21 + 3. **API Server** - Exposes XRPC endpoints that Spark clients call to fetch feed 22 + content (`so.sprk.feed.getFeedSkeleton`, 23 + `so.sprk.feed.describeFeedGenerator`). You won't need to modify these when 24 + creating a feed. 24 25 25 26 ## Creating Custom Feeds 26 27 27 - **For topic/community feeds:** Filter posts at the ingester level. Modify 28 - the handler in `ingester/handlers/post.ts` to only index posts matching your 28 + **For topic/community feeds:** Filter posts at the ingester level. Modify the 29 + handler in `ingester/handlers/post.ts` to only index posts matching your 29 30 criteria (hashtags, keywords, specific authors, etc.). 30 31 31 - **For personalized/sorted feeds:** Create a new algorithm in `algos/`. Copy 32 - `simple-desc.ts` as a starting point, then modify the query logic. Register 33 - your algorithm in `algos/index.ts`. 32 + **For personalized/sorted feeds:** Create a new algorithm in `algos/`. Copy 33 + `simple-desc.ts` as a starting point, then modify the query logic. Register your 34 + algorithm in `algos/index.ts`. 34 35 35 36 Example algorithm structure: 37 + 36 38 ```ts 37 39 // algos/my-feed.ts 38 40 export const info = { ··· 40 42 // Query posts from ctx.db.models.Post 41 43 // Return { cursor, feed: [{ post: "at://..." }] } 42 44 }, 43 - needsAuth: false, // Set true if feed needs user's DID 45 + needsAuth: false, // Set true if feed needs user's DID 44 46 publisherDid: "did:plc:your-did", 45 - rkey: "my-feed", // at://your-did/so.sprk.feed.generator/my-feed 47 + rkey: "my-feed", // at://your-did/so.sprk.feed.generator/my-feed 46 48 } as Algorithm; 47 49 ``` 48 50 ··· 97 99 98 100 ## Endpoints 99 101 100 - | Endpoint | Description | 101 - |----------|-------------| 102 - | `GET /` | Service info | 103 - | `GET /health` | Health check | 104 - | `GET /.well-known/did.json` | DID document for `did:web` resolution | 105 - | `GET /xrpc/so.sprk.feed.describeFeedGenerator` | List available feeds | 106 - | `GET /xrpc/so.sprk.feed.getFeedSkeleton` | Fetch feed posts | 102 + | Endpoint | Description | 103 + | ---------------------------------------------- | ------------------------------------- | 104 + | `GET /` | Service info | 105 + | `GET /health` | Health check | 106 + | `GET /.well-known/did.json` | DID document for `did:web` resolution | 107 + | `GET /xrpc/so.sprk.feed.describeFeedGenerator` | List available feeds | 108 + | `GET /xrpc/so.sprk.feed.getFeedSkeleton` | Fetch feed posts |
+46 -24
utils/auth.ts
··· 1 - import { MethodAuthContext, parseReqNsid, verifyJwt } from "@atp/xrpc-server"; 1 + import { MethodAuthContext, verifyJwt } from "@atp/xrpc-server"; 2 2 import { DidResolver } from "@atp/identity"; 3 3 4 4 export type NullOutput = { ··· 12 12 export type StandardOutput = { 13 13 credentials: { 14 14 type: "standard"; 15 - aud: string; 16 15 iss: string; 17 16 }; 18 17 artifacts: unknown; ··· 25 24 this.ownDid = ownDid ?? ""; 26 25 this.didResolver = didResolver; 27 26 } 27 + 28 28 standardOptional = async ( 29 29 ctx: MethodAuthContext, 30 30 ): Promise<StandardOutput | NullOutput> => { 31 - const authorization = ctx.req.headers.get("Authorization"); 32 - if (authorization?.startsWith("Bearer ") ?? false) { 33 - const jwt = authorization?.replace("Bearer ", "").trim(); 34 - const nsid = parseReqNsid(ctx.req); 35 - if (!jwt) return this.nullCreds(); 36 - const parsed = await verifyJwt( 37 - jwt, 38 - this.ownDid, 39 - nsid, 40 - async (did: string) => { 41 - return await this.didResolver.resolveAtprotoKey(did); 42 - }, 43 - ); 44 - return { 45 - credentials: { 46 - type: "standard", 47 - aud: parsed.aud, 48 - iss: parsed.iss, 49 - }, 50 - artifacts: null, 51 - }; 52 - } else { 31 + try { 32 + const authorization = ctx.req.headers.get("Authorization") ?? ""; 33 + 34 + if (!authorization) { 35 + return this.nullCreds(); 36 + } 37 + 38 + // Check for Bearer token 39 + const BEARER = "Bearer "; 40 + if (authorization.startsWith(BEARER)) { 41 + const jwt = authorization.replace(BEARER, "").trim(); 42 + 43 + try { 44 + const parsed = await verifyJwt( 45 + jwt, 46 + null, 47 + null, 48 + async (did: string) => { 49 + return await this.didResolver.resolveAtprotoKey(did); 50 + }, 51 + ); 52 + return { 53 + credentials: { 54 + type: "standard", 55 + iss: parsed.iss, 56 + }, 57 + artifacts: null, 58 + }; 59 + } catch (error) { 60 + // Log JWT verification errors for debugging 61 + console.error("JWT verification failed:", error); 62 + console.error( 63 + "JWT preview:", 64 + jwt.length > 20 ? jwt.substring(0, 20) + "..." : jwt, 65 + ); 66 + console.error("ownDid:", this.ownDid); 67 + // If JWT verification fails, treat as unauthenticated 68 + return this.nullCreds(); 69 + } 70 + } else { 71 + return this.nullCreds(); 72 + } 73 + } catch (error) { 74 + console.error("Unexpected error in standardOptional:", error); 53 75 return this.nullCreds(); 54 76 } 55 77 };