[READ ONLY MIRROR] Spark Social AppView Server github.com/sprksocial/server
atproto deno hono lexicon
1
fork

Configure Feed

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

labelers pipeline (#55)

* labelers pipeline

* fix nsid

authored by

Roscoe Rubin-Rottenberg and committed by
GitHub
63e990cd 7a239aea

+661 -79
+8 -7
api/so/sprk/actor/getProfile.ts
··· 15 15 const getProfile = createPipeline(skeleton, hydration, noRules, presentation); 16 16 server.so.sprk.actor.getProfile({ 17 17 auth: ctx.authVerifier.optionalStandardOrRole, 18 - handler: async ({ auth, params }) => { 18 + handler: async ({ auth, params, req }) => { 19 19 const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 20 - const hydrateCtx = ctx.hydrator.createContext({ 20 + const labelers = ctx.reqLabelers(req); 21 + const hydrateCtx = await ctx.hydrator.createContext({ 22 + labelers, 21 23 viewer, 22 24 includeTakedowns, 23 25 }); 24 26 25 - // Parallelize pipeline execution with repoRev fetch 26 - const [result, repoRev] = await Promise.all([ 27 - getProfile({ ...params, hydrateCtx }, ctx), 28 - ctx.hydrator.actor.getRepoRevSafe(viewer), 29 - ]); 27 + const result = await getProfile({ ...params, hydrateCtx }, ctx); 28 + 29 + const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer); 30 30 31 31 return { 32 32 encoding: "application/json", 33 33 body: result, 34 34 headers: resHeaders({ 35 35 repoRev, 36 + labelers: hydrateCtx.labelers, 36 37 }), 37 38 }; 38 39 },
+8 -7
api/so/sprk/actor/getProfiles.ts
··· 15 15 const getProfile = createPipeline(skeleton, hydration, noRules, presentation); 16 16 server.so.sprk.actor.getProfiles({ 17 17 auth: ctx.authVerifier.standardOptional, 18 - handler: async ({ auth, params }) => { 18 + handler: async ({ auth, params, req }) => { 19 19 const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 20 - const hydrateCtx = ctx.hydrator.createContext({ 20 + const labelers = ctx.reqLabelers(req); 21 + const hydrateCtx = await ctx.hydrator.createContext({ 21 22 viewer, 23 + labelers, 22 24 includeTakedowns, 23 25 }); 24 26 25 - // Parallelize pipeline execution with repoRev fetch 26 - const [result, repoRev] = await Promise.all([ 27 - getProfile({ ...params, hydrateCtx }, ctx), 28 - ctx.hydrator.actor.getRepoRevSafe(viewer), 29 - ]); 27 + const result = await getProfile({ ...params, hydrateCtx }, ctx); 28 + 29 + const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer); 30 30 31 31 return { 32 32 encoding: "application/json", 33 33 body: result, 34 34 headers: resHeaders({ 35 35 repoRev, 36 + labelers: hydrateCtx.labelers, 36 37 }), 37 38 }; 38 39 },
+7 -3
api/so/sprk/actor/searchActors.ts
··· 24 24 ); 25 25 server.so.sprk.actor.searchActors({ 26 26 auth: ctx.authVerifier.standardOptional, 27 - handler: async ({ auth, params }) => { 27 + handler: async ({ auth, params, req }) => { 28 28 const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 29 - const hydrateCtx = ctx.hydrator.createContext({ 29 + const labelers = ctx.reqLabelers(req); 30 + const hydrateCtx = await ctx.hydrator.createContext({ 30 31 viewer, 32 + labelers, 31 33 includeTakedowns, 32 34 }); 33 35 const results = await searchActors({ ...params, hydrateCtx }, ctx); 34 36 return { 35 37 encoding: "application/json", 36 38 body: results, 37 - headers: resHeaders({}), 39 + headers: resHeaders({ 40 + labelers: hydrateCtx.labelers, 41 + }), 38 42 }; 39 43 }, 40 44 });
+4 -2
api/so/sprk/feed/getAuthorFeed.ts
··· 27 27 ); 28 28 server.so.sprk.feed.getAuthorFeed({ 29 29 auth: ctx.authVerifier.optionalStandardOrRole, 30 - handler: async ({ params, auth }) => { 30 + handler: async ({ params, auth, req }) => { 31 31 const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 32 - const hydrateCtx = ctx.hydrator.createContext({ 32 + const labelers = ctx.reqLabelers(req); 33 + const hydrateCtx = await ctx.hydrator.createContext({ 34 + labelers, 33 35 viewer, 34 36 includeTakedowns, 35 37 });
+2 -1
api/so/sprk/feed/getFeed.ts
··· 55 55 }), 56 56 handler: async ({ params, auth, req }) => { 57 57 const viewer = auth.credentials.iss; 58 - const hydrateCtx = ctx.hydrator.createContext({ viewer }); 58 + const labelers = ctx.reqLabelers(req); 59 + const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers }); 59 60 const headers = noUndefinedVals({ 60 61 "user-agent": SPRK_USER_AGENT, 61 62 authorization: req.headers.get("authorization") as string,
+4 -2
api/so/sprk/feed/getFeedGenerator.ts
··· 5 5 export default function (server: Server, ctx: AppContext) { 6 6 server.so.sprk.feed.getFeedGenerator({ 7 7 auth: ctx.authVerifier.optionalStandardOrRole, 8 - handler: async ({ params, auth }) => { 8 + handler: async ({ params, auth, req }) => { 9 9 const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 10 - const hydrateCtx = ctx.hydrator.createContext({ 10 + const labelers = ctx.reqLabelers(req); 11 + const hydrateCtx = await ctx.hydrator.createContext({ 12 + labelers, 11 13 viewer, 12 14 includeTakedowns, 13 15 });
+4 -2
api/so/sprk/feed/getFeedGenerators.ts
··· 5 5 export default function (server: Server, ctx: AppContext) { 6 6 server.so.sprk.feed.getFeedGenerators({ 7 7 auth: ctx.authVerifier.optionalStandardOrRole, 8 - handler: async ({ params, auth }) => { 8 + handler: async ({ params, auth, req }) => { 9 9 const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 10 - const hydrateCtx = ctx.hydrator.createContext({ 10 + const labelers = ctx.reqLabelers(req); 11 + const hydrateCtx = await ctx.hydrator.createContext({ 12 + labelers, 11 13 viewer, 12 14 includeTakedowns, 13 15 });
+4 -2
api/so/sprk/feed/getPostThread.ts
··· 29 29 ); 30 30 server.so.sprk.feed.getPostThread({ 31 31 auth: ctx.authVerifier.optionalStandardOrRole, 32 - handler: async ({ params, auth, res }) => { 32 + handler: async ({ params, auth, req, res }) => { 33 33 const { viewer, includeTakedowns, include3pBlocks } = ctx.authVerifier 34 34 .parseCreds(auth); 35 - const hydrateCtx = ctx.hydrator.createContext({ 35 + const labelers = ctx.reqLabelers(req); 36 + const hydrateCtx = await ctx.hydrator.createContext({ 37 + labelers, 36 38 viewer, 37 39 includeTakedowns, 38 40 include3pBlocks,
+3 -2
api/so/sprk/feed/getPosts.ts
··· 16 16 const getPosts = createPipeline(skeleton, hydration, noBlocks, presentation); 17 17 server.so.sprk.feed.getPosts({ 18 18 auth: ctx.authVerifier.standardOptional, 19 - handler: async ({ params, auth }) => { 19 + handler: async ({ params, auth, req }) => { 20 20 const viewer = auth.credentials.iss; 21 - const hydrateCtx = ctx.hydrator.createContext({ viewer }); 21 + const labelers = ctx.reqLabelers(req); 22 + const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers }); 22 23 23 24 const results = await getPosts({ ...params, hydrateCtx }, ctx); 24 25
+3 -2
api/so/sprk/feed/getSuggestedFeeds.ts
··· 7 7 export default function (server: Server, ctx: AppContext) { 8 8 server.so.sprk.feed.getSuggestedFeeds({ 9 9 auth: ctx.authVerifier.standardOptional, 10 - handler: async ({ auth, params }) => { 10 + handler: async ({ auth, params, req }) => { 11 11 const viewer = auth.credentials.iss; 12 12 13 13 // @NOTE no need to coordinate the cursor for appview swap, as v1 doesn't use the cursor ··· 16 16 params.cursor, 17 17 ); 18 18 const uris = suggestedRes.uris; 19 - const hydrateCtx = ctx.hydrator.createContext({ viewer }); 19 + const labelers = ctx.reqLabelers(req); 20 + const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers }); 20 21 const hydration = await ctx.hydrator.hydrateFeedGens(uris, hydrateCtx); 21 22 const feedViews = mapDefined( 22 23 uris,
+3 -2
api/so/sprk/feed/getTimeline.ts
··· 23 23 ); 24 24 server.so.sprk.feed.getTimeline({ 25 25 auth: ctx.authVerifier.standard, 26 - handler: async ({ params, auth }) => { 26 + handler: async ({ params, auth, req }) => { 27 27 const viewer = auth.credentials.iss; 28 - const hydrateCtx = ctx.hydrator.createContext({ viewer }); 28 + const labelers = ctx.reqLabelers(req); 29 + const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers }); 29 30 30 31 // Parallelize pipeline execution with repoRev fetch 31 32 const [result, repoRev] = await Promise.all([
+3 -2
api/so/sprk/feed/searchPosts.ts
··· 30 30 ); 31 31 server.so.sprk.feed.searchPosts({ 32 32 auth: ctx.authVerifier.standardOptional, 33 - handler: async ({ auth, params }) => { 33 + handler: async ({ auth, params, req }) => { 34 34 const { viewer } = ctx.authVerifier.parseCreds(auth); 35 - const hydrateCtx = ctx.hydrator.createContext({ viewer }); 35 + const labelers = ctx.reqLabelers(req); 36 + const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers }); 36 37 const results = await searchPosts( 37 38 { ...params, hydrateCtx }, 38 39 ctx,
+4 -2
api/so/sprk/graph/getFollowers.ts
··· 28 28 ); 29 29 server.so.sprk.graph.getFollowers({ 30 30 auth: ctx.authVerifier.optionalStandardOrRole, 31 - handler: async ({ params, auth }) => { 31 + handler: async ({ params, auth, req }) => { 32 32 const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 33 - const hydrateCtx = ctx.hydrator.createContext({ 33 + const labelers = ctx.reqLabelers(req); 34 + const hydrateCtx = await ctx.hydrator.createContext({ 35 + labelers, 34 36 viewer, 35 37 includeTakedowns, 36 38 });
+4 -2
api/so/sprk/graph/getFollows.ts
··· 27 27 ); 28 28 server.so.sprk.graph.getFollows({ 29 29 auth: ctx.authVerifier.optionalStandardOrRole, 30 - handler: async ({ params, auth }) => { 30 + handler: async ({ params, auth, req }) => { 31 31 const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 32 - const hydrateCtx = ctx.hydrator.createContext({ 32 + const labelers = ctx.reqLabelers(req); 33 + const hydrateCtx = await ctx.hydrator.createContext({ 34 + labelers, 33 35 viewer, 34 36 includeTakedowns, 35 37 });
+6 -2
api/so/sprk/sound/getActorAudios.ts
··· 23 23 ); 24 24 server.so.sprk.sound.getActorAudios({ 25 25 auth: ctx.authVerifier.standardOptional, 26 - handler: async ({ params, auth }) => { 26 + handler: async ({ params, auth, req }) => { 27 27 const viewer = auth.credentials.type === "standard" 28 28 ? auth.credentials.iss 29 29 : undefined; 30 - const hydrateCtx = ctx.hydrator.createContext({ viewer: viewer ?? null }); 30 + const labelers = ctx.reqLabelers(req); 31 + const hydrateCtx = await ctx.hydrator.createContext({ 32 + viewer: viewer ?? null, 33 + labelers, 34 + }); 31 35 32 36 const results = await getActorAudios({ ...params, hydrateCtx }, ctx); 33 37
+6 -2
api/so/sprk/sound/getAudioPosts.ts
··· 24 24 ); 25 25 server.so.sprk.sound.getAudioPosts({ 26 26 auth: ctx.authVerifier.standardOptional, 27 - handler: async ({ params, auth }) => { 27 + handler: async ({ params, auth, req }) => { 28 28 const viewer = auth.credentials.type === "standard" 29 29 ? auth.credentials.iss 30 30 : undefined; 31 - const hydrateCtx = ctx.hydrator.createContext({ viewer: viewer ?? null }); 31 + const labelers = ctx.reqLabelers(req); 32 + const hydrateCtx = await ctx.hydrator.createContext({ 33 + viewer: viewer ?? null, 34 + labelers, 35 + }); 32 36 33 37 const results = await getAudioPosts({ ...params, hydrateCtx }, ctx); 34 38
+6 -2
api/so/sprk/sound/getAudios.ts
··· 16 16 const getAudios = createPipeline(skeleton, hydration, noBlocks, presentation); 17 17 server.so.sprk.sound.getAudios({ 18 18 auth: ctx.authVerifier.standardOptional, 19 - handler: async ({ params, auth }) => { 19 + handler: async ({ params, auth, req }) => { 20 20 const viewer = auth.credentials.type === "standard" 21 21 ? auth.credentials.iss 22 22 : undefined; 23 - const hydrateCtx = ctx.hydrator.createContext({ viewer: viewer ?? null }); 23 + const labelers = ctx.reqLabelers(req); 24 + const hydrateCtx = await ctx.hydrator.createContext({ 25 + viewer: viewer ?? null, 26 + labelers, 27 + }); 24 28 25 29 const results = await getAudios({ ...params, hydrateCtx }, ctx); 26 30
+6 -2
api/so/sprk/sound/getTrendingAudios.ts
··· 22 22 ); 23 23 server.so.sprk.sound.getTrendingAudios({ 24 24 auth: ctx.authVerifier.standardOptional, 25 - handler: async ({ params, auth }) => { 25 + handler: async ({ params, auth, req }) => { 26 26 const viewer = auth.credentials.type === "standard" 27 27 ? auth.credentials.iss 28 28 : undefined; 29 - const hydrateCtx = ctx.hydrator.createContext({ viewer: viewer ?? null }); 29 + const labelers = ctx.reqLabelers(req); 30 + const hydrateCtx = await ctx.hydrator.createContext({ 31 + viewer: viewer ?? null, 32 + labelers, 33 + }); 30 34 31 35 const results = await getTrendingAudios({ ...params, hydrateCtx }, ctx); 32 36
+3 -2
api/so/sprk/story/getStories.ts
··· 57 57 const getStories = createPipeline(skeleton, hydration, rules, presentation); 58 58 server.so.sprk.story.getStories({ 59 59 auth: ctx.authVerifier.standardOptional, 60 - handler: async ({ params, auth }) => { 60 + handler: async ({ params, auth, req }) => { 61 61 const viewer = auth.credentials.type === "standard" 62 62 ? auth.credentials.iss 63 63 : null; 64 - const hydrateCtx = ctx.hydrator.createContext({ viewer }); 64 + const labelers = ctx.reqLabelers(req); 65 + const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers }); 65 66 66 67 // Ensure uris is an array 67 68 const uriArray = Array.isArray(params.uris) ? params.uris : [params.uris];
+3 -2
api/so/sprk/story/getTimeline.ts
··· 30 30 ); 31 31 server.so.sprk.story.getTimeline({ 32 32 auth: ctx.authVerifier.standard, 33 - handler: async ({ params, auth }) => { 33 + handler: async ({ params, auth, req }) => { 34 34 const viewer = auth.credentials.iss; 35 - const hydrateCtx = ctx.hydrator.createContext({ viewer }); 35 + const labelers = ctx.reqLabelers(req); 36 + const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers }); 36 37 37 38 const { limit: limitParam = DEFAULT_LIMIT, cursor } = params; 38 39
+6
api/util.ts
··· 1 + import { formatLabelerHeader, ParsedLabelers } from "../util.ts"; 2 + 1 3 export const SPRK_USER_AGENT = "SprkAppView"; 2 4 export const ATPROTO_CONTENT_LABELERS = "Atproto-Content-Labelers"; 3 5 export const ATPROTO_REPO_REV = "Atproto-Repo-Rev"; 4 6 5 7 type ResHeaderOpts = { 8 + labelers: ParsedLabelers; 6 9 repoRev: string | null; 7 10 }; 8 11 ··· 10 13 opts: Partial<ResHeaderOpts>, 11 14 ): Record<string, string> => { 12 15 const headers: Record<string, string> = {}; 16 + if (opts.labelers) { 17 + headers[ATPROTO_CONTENT_LABELERS] = formatLabelerHeader(opts.labelers); 18 + } 13 19 if (opts.repoRev) { 14 20 headers[ATPROTO_REPO_REV] = opts.repoRev; 15 21 }
+9
config.ts
··· 30 30 dbPass?: string; 31 31 relayUrl?: string; 32 32 plcUrl?: string; 33 + 34 + labelsFromIssuerDids: string[]; 33 35 } 34 36 35 37 export class ServerConfig { ··· 70 72 "wss://relay1.us-east.bsky.network"; 71 73 const plcUrl = envStr("SPRK_PLC") ?? "https://plc.directory"; 72 74 75 + const labelsFromIssuerDids = envList("SPRK_LABELS_FROM_ISSUER_DIDS") ?? []; 76 + 73 77 return new ServerConfig({ 74 78 version, 75 79 debugMode, ··· 94 98 dbPass, 95 99 relayUrl, 96 100 plcUrl, 101 + labelsFromIssuerDids, 97 102 }); 98 103 } 99 104 ··· 170 175 } 171 176 get plcUrl() { 172 177 return this.cfg.plcUrl; 178 + } 179 + 180 + get labelsFromIssuerDids() { 181 + return this.cfg.labelsFromIssuerDids; 173 182 } 174 183 }
+2
context.ts
··· 6 6 import { IdResolver } from "@atp/identity"; 7 7 import { AuthVerifier } from "./auth-verifier.ts"; 8 8 import { ServerConfig } from "./config.ts"; 9 + import { ParsedLabelers } from "./util.ts"; 9 10 10 11 export type AppContext = { 11 12 db: Database; ··· 16 17 idResolver: IdResolver; 17 18 authVerifier: AuthVerifier; 18 19 cfg: ServerConfig; 20 + reqLabelers: (req: Request) => ParsedLabelers; 19 21 }; 20 22 21 23 export type AppEnv = {
+8
data-plane/db/index.ts
··· 100 100 "Generator", 101 101 models.generatorSchema, 102 102 ), 103 + Labeler: this.connection.model<models.LabelerDocument>( 104 + "Labeler", 105 + models.labelerSchema, 106 + ), 107 + Label: this.connection.model<models.LabelDocument>( 108 + "Label", 109 + models.labelSchema, 110 + ), 103 111 Takedown: this.connection.model<models.TakedownDocument>( 104 112 "Takedown", 105 113 models.takedownSchema,
+36
data-plane/db/models.ts
··· 386 386 }) 387 387 .index({ authorDid: 1, createdAt: -1 }); 388 388 389 + // labelers 390 + 391 + export interface LabelerDocument extends AuthoredDocument {} 392 + 393 + export const labelerSchema = new Schema<LabelerDocument>({ 394 + ...authoredSchema, 395 + }) 396 + .index({ authorDid: 1, createdAt: -1 }); 397 + 398 + // labels 399 + 400 + export interface LabelDocument extends Document { 401 + src: string; 402 + uri: string; 403 + cid: string; 404 + val: string; 405 + neg: boolean; 406 + cts: string; 407 + exp: string | null; 408 + } 409 + 410 + export const labelSchema = new Schema<LabelDocument>({ 411 + src: { type: String, required: true, index: true }, 412 + uri: { type: String, required: true, index: true }, 413 + cid: { type: String, required: true }, 414 + val: { type: String, required: true, index: true }, 415 + neg: { type: Boolean, required: true }, 416 + cts: { type: String, required: true }, 417 + exp: { type: String, required: false, default: null }, 418 + }) 419 + .index({ uri: 1, src: 1, val: 1 }, { unique: true }) 420 + .index({ src: 1, cts: -1 }); 421 + 389 422 // takedowns 390 423 391 424 export interface TakedownDocument extends Document { ··· 569 602 generatorSchema, 570 603 audioSchema, 571 604 storySchema, 605 + labelerSchema, 572 606 ] as Schema[]).forEach((s) => s.plugin(addAuthor)); 573 607 574 608 export interface DatabaseModels { ··· 584 618 Audio: Model<AudioDocument>; 585 619 Repost: Model<RepostDocument>; 586 620 Generator: Model<GeneratorDocument>; 621 + Labeler: Model<LabelerDocument>; 622 + Label: Model<LabelDocument>; 587 623 Takedown: Model<TakedownDocument>; 588 624 RepoTakedown: Model<RepoTakedownDocument>; 589 625 BlobTakedown: Model<BlobTakedownDocument>;
+3
data-plane/index.ts
··· 19 19 import { Threads } from "./routes/threads.ts"; 20 20 import { Preferences } from "./routes/preferences.ts"; 21 21 import { Search } from "./routes/search.ts"; 22 + import { Labels } from "./routes/labels.ts"; 22 23 23 24 export { RepoSubscription } from "./subscription.ts"; 24 25 ··· 51 52 public threads: Threads; 52 53 public preferences: Preferences; 53 54 public search: Search; 55 + public labels: Labels; 54 56 55 57 constructor( 56 58 db: Database, ··· 79 81 this.threads = new Threads(db); 80 82 this.preferences = new Preferences(db); 81 83 this.search = new Search(db); 84 + this.labels = new Labels(db); 82 85 } 83 86 }
+2 -4
data-plane/indexing/index.ts
··· 120 120 ); 121 121 } 122 122 123 - const uri = `at://${did}/so.sprk.actor.profile`; 124 - const actorInfo = { uri, handle, indexedAt: timestamp }; 123 + const actorInfo = { handle, indexedAt: timestamp }; 125 124 await this.db.models.Actor.findOneAndUpdate( 126 125 { did }, 127 126 { did, ...actorInfo }, ··· 135 134 ); 136 135 137 136 // Still update the actor record with null handle to prevent repeated attempts 138 - const uri = `at://${did}/so.sprk.actor.profile`; 139 - const actorInfo = { uri, handle: null, indexedAt: timestamp }; 137 + const actorInfo = { handle: null, indexedAt: timestamp }; 140 138 try { 141 139 await this.db.models.Actor.findOneAndUpdate( 142 140 { did },
+76
data-plane/indexing/plugins/labeler.ts
··· 1 + import { CID } from "multiformats/cid"; 2 + import { AtUri, normalizeDatetimeAlways } from "@atp/syntax"; 3 + import * as lex from "../../../lex/lexicons.ts"; 4 + import * as Labeler from "../../../lex/types/so/sprk/labeler/service.ts"; 5 + import { BackgroundQueue } from "../../background.ts"; 6 + import { Database } from "../../db/index.ts"; 7 + import { LabelerDocument } from "../../db/models.ts"; 8 + import { RecordProcessor } from "../processor.ts"; 9 + 10 + const lexId = lex.ids.SoSprkLabelerService; 11 + type IndexedLabeler = LabelerDocument; 12 + 13 + const insertFn = async ( 14 + db: Database, 15 + uri: AtUri, 16 + cid: CID, 17 + obj: Labeler.Record, 18 + timestamp: string, 19 + ): Promise<IndexedLabeler | null> => { 20 + if (uri.rkey !== "self") return null; 21 + 22 + const labeler = { 23 + uri: uri.toString(), 24 + cid: cid.toString(), 25 + authorDid: uri.host, 26 + createdAt: normalizeDatetimeAlways(obj.createdAt), 27 + indexedAt: timestamp, 28 + }; 29 + 30 + const insertedLabeler = await db.models.Labeler.findOneAndUpdate( 31 + { uri: labeler.uri }, 32 + { $set: labeler }, 33 + { upsert: true, new: true }, 34 + ); 35 + return insertedLabeler; 36 + }; 37 + 38 + const findDuplicate = (): AtUri | null => { 39 + return null; 40 + }; 41 + 42 + const notifsForInsert = () => { 43 + return []; 44 + }; 45 + 46 + const deleteFn = async ( 47 + db: Database, 48 + uri: AtUri, 49 + ): Promise<IndexedLabeler | null> => { 50 + const deleted = await db.models.Labeler.findOneAndDelete({ 51 + uri: uri.toString(), 52 + }); 53 + return deleted; 54 + }; 55 + 56 + const notifsForDelete = () => { 57 + return { notifs: [], toDelete: [] }; 58 + }; 59 + 60 + export type PluginType = RecordProcessor<Labeler.Record, IndexedLabeler>; 61 + 62 + export const makePlugin = ( 63 + db: Database, 64 + background: BackgroundQueue, 65 + ): PluginType => { 66 + return new RecordProcessor(db, background, { 67 + lexId, 68 + insertFn, 69 + findDuplicate, 70 + deleteFn, 71 + notifsForInsert, 72 + notifsForDelete, 73 + }); 74 + }; 75 + 76 + export default makePlugin;
+56
data-plane/routes/labels.ts
··· 1 + import { noUndefinedVals } from "@atp/common"; 2 + import { Database } from "../db/index.ts"; 3 + 4 + export class Labels { 5 + private db: Database; 6 + 7 + constructor(db: Database) { 8 + this.db = db; 9 + } 10 + 11 + async getLabels(subjects: string[], issuers: string[]) { 12 + if (subjects.length === 0 || issuers.length === 0) { 13 + return { labels: [] }; 14 + } 15 + 16 + const now = new Date().toISOString(); 17 + 18 + const res = await this.db.models.Label.find({ 19 + uri: { $in: subjects }, 20 + src: { $in: issuers }, 21 + $or: [ 22 + { exp: null }, 23 + { exp: { $gt: now } }, 24 + ], 25 + }).lean(); 26 + 27 + const labelsBySubject = new Map<string, typeof res>(); 28 + res.forEach((l) => { 29 + const labels = labelsBySubject.get(l.uri) ?? []; 30 + labels.push(l); 31 + labelsBySubject.set(l.uri, labels); 32 + }); 33 + 34 + // intentionally duplicate label results, appview frontend should be defensive to this 35 + const labels = subjects.flatMap((sub) => { 36 + const labelsForSub = labelsBySubject.get(sub) ?? []; 37 + return labelsForSub.map((l) => { 38 + return noUndefinedVals({ 39 + src: l.src, 40 + uri: l.uri, 41 + cid: l.cid === "" ? undefined : l.cid, 42 + val: l.val, 43 + neg: l.neg === true ? true : undefined, 44 + cts: l.cts, 45 + exp: l.exp === null ? undefined : l.exp, 46 + }); 47 + }); 48 + }); 49 + 50 + return { labels }; 51 + } 52 + 53 + getAllLabelers() { 54 + throw new Error("not implemented"); 55 + } 56 + }
+2 -1
deno.json
··· 32 32 "mongoose": "npm:mongoose@^8.20.2", 33 33 "multiformats": "npm:multiformats@^13.4.1", 34 34 "p-queue": "npm:p-queue@^8.1.1", 35 - "mongodb-memory-server-core": "npm:mongodb-memory-server-core@^11.0.0" 35 + "mongodb-memory-server-core": "npm:mongodb-memory-server-core@^11.0.0", 36 + "structured-headers": "npm:structured-headers@^2.0.2" 36 37 }, 37 38 "test": { 38 39 "permissions": {
+7 -2
deno.lock
··· 43 43 "npm:mongoose@^8.20.2": "8.20.2", 44 44 "npm:multiformats@^13.4.1": "13.4.1", 45 45 "npm:p-queue@^8.1.1": "8.1.1", 46 - "npm:rate-limiter-flexible@9": "9.0.0" 46 + "npm:rate-limiter-flexible@9": "9.0.0", 47 + "npm:structured-headers@^2.0.2": "2.0.2" 47 48 }, 48 49 "jsr": { 49 50 "@atp/bytes@0.1.0-alpha.1": { ··· 547 548 "text-decoder" 548 549 ] 549 550 }, 551 + "structured-headers@2.0.2": { 552 + "integrity": "sha512-IUul56vVHuMg2UxWhwDj9zVJE6ztYEQQkynr1FQ/NydPhivtk5+Qb2N1RS36owEFk2fNUriTguJ2R7htRObcdA==" 553 + }, 550 554 "tar-stream@3.1.7": { 551 555 "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", 552 556 "dependencies": [ ··· 627 631 "npm:mongodb-memory-server-core@11", 628 632 "npm:mongoose@^8.20.2", 629 633 "npm:multiformats@^13.4.1", 630 - "npm:p-queue@^8.1.1" 634 + "npm:p-queue@^8.1.1", 635 + "npm:structured-headers@^2.0.2" 631 636 ] 632 637 } 633 638 }
+119 -19
hydration/index.ts
··· 43 43 } from "./graph.ts"; 44 44 import { HydrationMap, ItemRef, mergeMaps, RecordInfo } from "./util.ts"; 45 45 import { getLogger } from "@logtape/logtape"; 46 + import { 47 + LabelerAggs, 48 + Labelers, 49 + LabelerViewerStates, 50 + LabelHydrator, 51 + Labels, 52 + } from "./label.ts"; 53 + import { ParsedLabelers } from "../util.ts"; 46 54 47 55 export class HydrateCtx { 56 + labelers: ParsedLabelers; 48 57 viewer: string | null; 49 58 includeTakedowns?: boolean; 50 59 includeActorTakedowns?: boolean; 51 60 include3pBlocks?: boolean; 52 61 53 62 constructor(private vals: HydrateCtxVals) { 63 + this.labelers = this.vals.labelers; 54 64 this.viewer = this.vals.viewer !== null 55 65 ? serviceRefToDid(this.vals.viewer) 56 66 : null; ··· 69 79 } 70 80 71 81 export type HydrateCtxVals = { 82 + labelers: ParsedLabelers; 72 83 viewer: string | null; 73 84 includeTakedowns?: boolean; 74 85 includeActorTakedowns?: boolean; ··· 96 107 followBlocks?: FollowBlocks; 97 108 likes?: Likes; 98 109 likeBlocks?: LikeBlocks; 110 + labels?: Labels; 99 111 feedgens?: FeedGens; 100 112 feedgenViewers?: FeedGenViewerStates; 101 113 feedgenAggs?: FeedGenAggs; 114 + labelers?: Labelers; 115 + labelerViewers?: LabelerViewerStates; 116 + labelerAggs?: LabelerAggs; 102 117 knownFollowers?: KnownFollowersStates; 103 118 activitySubscriptions?: ActivitySubscriptionStates; 104 119 bidirectionalBlocks?: BidirectionalBlocks; ··· 127 142 feed: FeedHydrator; 128 143 graph: GraphHydrator; 129 144 story: StoryHydrator; 145 + label: LabelHydrator; 146 + serviceLabelers: Set<string>; 130 147 131 148 constructor( 132 149 public dataplane: DataPlane, 150 + serviceLabelers: string[], 133 151 ) { 134 152 this.actor = new ActorHydrator(dataplane); 135 153 this.feed = new FeedHydrator(dataplane); 136 154 this.graph = new GraphHydrator(dataplane); 137 155 this.story = new StoryHydrator(dataplane); 156 + this.label = new LabelHydrator(dataplane); 157 + this.serviceLabelers = new Set(serviceLabelers); 138 158 } 139 159 140 160 // so.sprk.actor.defs#profileView ··· 165 185 ctx: HydrateCtx, 166 186 ): Promise<HydrationState> { 167 187 const includeTakedowns = ctx.includeTakedowns || ctx.includeActorTakedowns; 168 - const [actors, profileViewersState] = await Promise.all([ 188 + const [actors, labels, profileViewersState] = await Promise.all([ 169 189 this.actor.getActors(dids, { 170 190 includeTakedowns, 171 191 }), 192 + this.label.getLabelsForSubjects(labelSubjectsForDid(dids), ctx.labelers), 172 193 this.hydrateProfileViewers(dids, ctx), 173 194 ]); 195 + if (!includeTakedowns) { 196 + actionTakedownLabels(dids, actors, labels); 197 + } 174 198 return mergeStates(profileViewersState ?? {}, { 175 199 actors, 200 + labels, 176 201 ctx, 177 202 }); 178 203 } ··· 261 286 new AtUri(ref.uri).collection === ids.SoSprkFeedReply 262 287 ); 263 288 289 + const allUris = refs.map((ref) => ref.uri); 290 + 264 291 state.posts ??= new HydrationMap<Post>(); 265 292 state.replies ??= new HydrationMap<Reply>(); 266 293 ··· 339 366 postAggs, 340 367 replyAggs, 341 368 postViewers, 369 + labels, 342 370 postBlocks, 343 371 profileState, 344 372 threadContexts, ··· 349 377 ctx.viewer 350 378 ? this.feed.getPostViewerStates(threadRefs, ctx.viewer) 351 379 : Promise.resolve<PostViewerStates | undefined>(undefined), 380 + this.label.getLabelsForSubjects(allUris, ctx.labelers), 352 381 this.hydratePostBlocks(state.posts!, state.replies!), 353 382 this.hydrateProfiles(authorDids, ctx), 354 383 this.feed.getThreadContexts(threadRefs), ··· 365 394 replyAggs, 366 395 postViewers, 367 396 postBlocks, 397 + labels, 368 398 threadContexts, 369 399 ctx, 370 400 }, ··· 531 561 uris: string[], // @TODO any way to get refs here? 532 562 ctx: HydrateCtx, 533 563 ): Promise<HydrationState> { 534 - const [feedgens, feedgenAggs, feedgenViewers, profileState] = await Promise 535 - .all([ 536 - this.feed.getFeedGens(uris, ctx.includeTakedowns), 537 - this.feed.getFeedGenAggregates( 538 - uris.map((uri) => ({ uri })), 539 - ), 540 - ctx.viewer 541 - ? this.feed.getFeedGenViewerStates(uris, ctx.viewer) 542 - : undefined, 543 - this.hydrateProfiles(uris.map(didFromUri), ctx), 544 - ]); 564 + const [feedgens, feedgenAggs, feedgenViewers, profileState, labels] = 565 + await Promise 566 + .all([ 567 + this.feed.getFeedGens(uris, ctx.includeTakedowns), 568 + this.feed.getFeedGenAggregates( 569 + uris.map((uri) => ({ uri })), 570 + ), 571 + ctx.viewer 572 + ? this.feed.getFeedGenViewerStates(uris, ctx.viewer) 573 + : undefined, 574 + this.hydrateProfiles(uris.map(didFromUri), ctx), 575 + this.label.getLabelsForSubjects(uris, ctx.labelers), 576 + ]); 545 577 return mergeStates(profileState, { 546 578 feedgens, 547 579 feedgenAggs, 548 580 feedgenViewers, 581 + labels, 549 582 ctx, 550 583 }); 551 584 } ··· 677 710 return result; 678 711 } 679 712 713 + // so.sprk.labeler.def#labelerViewDetailed 714 + // - labeler 715 + // - profile 716 + // - list basic 717 + async hydrateLabelers( 718 + dids: string[], 719 + ctx: HydrateCtx, 720 + ): Promise<HydrationState> { 721 + const [labelers, labelerAggs, labelerViewers, profileState] = await Promise 722 + .all([ 723 + this.label.getLabelers(dids, ctx.includeTakedowns), 724 + this.label.getLabelerAggregates(dids, ctx.viewer), 725 + ctx.viewer 726 + ? this.label.getLabelerViewerStates(dids, ctx.viewer) 727 + : undefined, 728 + this.hydrateProfiles(dids, ctx), 729 + ]); 730 + actionTakedownLabels(dids, labelers, profileState.labels ?? new Labels()); 731 + return mergeStates(profileState, { 732 + labelers, 733 + labelerAggs, 734 + labelerViewers, 735 + ctx, 736 + }); 737 + } 738 + 680 739 // ad-hoc record hydration 681 740 // in com.atproto.repo.getRecord 682 741 async getRecord(uri: string, includeTakedowns = false) { ··· 692 751 (await this.feed.getReplies([uri], includeTakedowns)).get(uri) ?? 693 752 undefined 694 753 ); 695 - } else if (collection === ids.AppBskyFeedRepost) { 754 + } else if (collection === ids.SoSprkFeedRepost) { 696 755 return ( 697 756 (await this.feed.getReposts([uri], includeTakedowns)).get(uri) ?? 698 757 undefined ··· 717 776 (await this.graph.getBlocks([uri], includeTakedowns)).get(uri) ?? 718 777 undefined 719 778 ); 720 - } else if ( 721 - collection === ids.AppBskyFeedGenerator || 722 - collection === ids.SoSprkFeedGenerator 723 - ) { 779 + } else if (collection === ids.SoSprkFeedGenerator) { 724 780 return ( 725 781 (await this.feed.getFeedGens([uri], includeTakedowns)).get(uri) ?? 782 + undefined 783 + ); 784 + } else if (collection === ids.SoSprkLabelerService) { 785 + if (parsed.rkey !== "self") return; 786 + const did = parsed.hostname; 787 + return ( 788 + (await this.label.getLabelers([did], includeTakedowns)).get(did) ?? 726 789 undefined 727 790 ); 728 791 } else if (collection === ids.SoSprkActorProfile) { ··· 767 830 } 768 831 } 769 832 770 - createContext = (vals: HydrateCtxVals) => { 833 + async createContext(vals: HydrateCtxVals) { 834 + // ensures we're only apply labelers that exist and are not taken down 835 + const labelers = vals.labelers.dids; 836 + const nonServiceLabelers = labelers.filter( 837 + (did) => !this.serviceLabelers.has(did), 838 + ); 839 + const labelerActors = await this.actor.getActors(nonServiceLabelers, { 840 + includeTakedowns: vals.includeTakedowns, 841 + }); 842 + const availableDids = labelers.filter( 843 + (did) => this.serviceLabelers.has(did) || !!labelerActors.get(did), 844 + ); 845 + const availableLabelers = { 846 + dids: availableDids, 847 + redact: vals.labelers.redact, 848 + }; 771 849 return new HydrateCtx({ 850 + labelers: availableLabelers, 772 851 viewer: vals.viewer, 773 852 includeTakedowns: vals.includeTakedowns, 774 853 include3pBlocks: vals.include3pBlocks, 775 854 }); 776 - }; 855 + } 777 856 778 857 async resolveUri(uriStr: string) { 779 858 const uri = new AtUri(uriStr); ··· 788 867 const serviceRefToDid = (serviceRef: string) => { 789 868 const idx = serviceRef.indexOf("#"); 790 869 return idx !== -1 ? serviceRef.slice(0, idx) : serviceRef; 870 + }; 871 + 872 + const labelSubjectsForDid = (dids: string[]) => { 873 + return [ 874 + ...dids, 875 + ...dids.map((did) => 876 + AtUri.make(did, ids.SoSprkActorProfile, "self").toString() 877 + ), 878 + ]; 791 879 }; 792 880 793 881 const rootUrisFromReplies = (replies: Replies): string[] => { ··· 858 946 export const mergeManyStates = (...states: HydrationState[]) => { 859 947 return states.reduce(mergeStates, {} as HydrationState); 860 948 }; 949 + 950 + const actionTakedownLabels = <T>( 951 + keys: string[], 952 + hydrationMap: HydrationMap<T>, 953 + labels: Labels, 954 + ) => { 955 + for (const key of keys) { 956 + if (labels.get(key)?.isTakendown) { 957 + hydrationMap.set(key, null); 958 + } 959 + } 960 + };
+174
hydration/label.ts
··· 1 + import { AtUri } from "@atp/syntax"; 2 + import { DataPlane } from "../data-plane/index.ts"; 3 + import { ids } from "../lex/lexicons.ts"; 4 + import { Record as LabelerRecord } from "../lex/types/app/bsky/labeler/service.ts"; 5 + import { Label } from "../lex/types/com/atproto/label/defs.ts"; 6 + import { ParsedLabelers } from "../util.ts"; 7 + import { 8 + HydrationMap, 9 + Merges, 10 + parseRecord, 11 + parseString, 12 + RecordInfo, 13 + } from "./util.ts"; 14 + 15 + export type { Label } from "../lex/types/com/atproto/label/defs.ts"; 16 + 17 + export type SubjectLabels = { 18 + isImpersonation: boolean; 19 + isTakendown: boolean; 20 + needsReview: boolean; 21 + labels: HydrationMap<Label>; // src + val -> label 22 + }; 23 + 24 + export class Labels extends HydrationMap<SubjectLabels> implements Merges { 25 + static key(label: Label) { 26 + return `${label.src}::${label.val}`; 27 + } 28 + override merge(map: Labels): this { 29 + map.forEach((theirs, key) => { 30 + if (!theirs) return; 31 + const mine = this.get(key); 32 + if (mine) { 33 + mine.isTakendown = mine.isTakendown || theirs.isTakendown; 34 + mine.labels = mine.labels.merge(theirs.labels); 35 + } else { 36 + this.set(key, theirs); 37 + } 38 + }); 39 + return this; 40 + } 41 + getBySubject(sub: string): Label[] { 42 + const it = this.get(sub)?.labels.values(); 43 + if (!it) return []; 44 + const labels: Label[] = []; 45 + for (const label of it) { 46 + if (label) labels.push(label); 47 + } 48 + return labels; 49 + } 50 + } 51 + 52 + export type LabelerAgg = { 53 + likes: number; 54 + }; 55 + 56 + export type LabelerAggs = HydrationMap<LabelerAgg>; 57 + 58 + export type Labeler = RecordInfo<LabelerRecord>; 59 + export type Labelers = HydrationMap<Labeler>; 60 + 61 + export type LabelerViewerState = { 62 + like?: string; 63 + }; 64 + 65 + export type LabelerViewerStates = HydrationMap<LabelerViewerState>; 66 + 67 + export class LabelHydrator { 68 + constructor(public dataplane: DataPlane) {} 69 + 70 + async getLabelsForSubjects( 71 + subjects: string[], 72 + labelers: ParsedLabelers, 73 + ): Promise<Labels> { 74 + if (!subjects.length || !labelers.dids.length) return new Labels(); 75 + const res = await this.dataplane.labels.getLabels( 76 + subjects, 77 + labelers.dids, 78 + ); 79 + 80 + return res.labels.reduce((acc, cur) => { 81 + const label = cur as unknown as Label; 82 + if (!label || label.neg) return acc; 83 + const { sig: _, ...labelWithoutSig } = label; 84 + let entry = acc.get(label.uri); 85 + if (!entry) { 86 + entry = { 87 + isImpersonation: false, 88 + isTakendown: false, 89 + needsReview: false, 90 + labels: new HydrationMap(), 91 + }; 92 + acc.set(label.uri, entry); 93 + } 94 + 95 + const isActionableNeedsReview = label.val === NEEDS_REVIEW_LABEL && 96 + !label.neg && 97 + labelers.redact.has(label.src); 98 + 99 + // we action needs review labels on backend for now so don't send to client until client has proper logic for them 100 + if (!isActionableNeedsReview) { 101 + entry.labels.set(Labels.key(labelWithoutSig), labelWithoutSig); 102 + } 103 + 104 + if ( 105 + TAKEDOWN_LABELS.includes(label.val) && 106 + !label.neg && 107 + labelers.redact.has(label.src) 108 + ) { 109 + entry.isTakendown = true; 110 + } 111 + if (isActionableNeedsReview) { 112 + entry.needsReview = true; 113 + } 114 + if ( 115 + label.val === IMPERSONATION_LABEL && 116 + !label.neg && 117 + labelers.redact.has(label.src) 118 + ) { 119 + entry.isImpersonation = true; 120 + } 121 + 122 + return acc; 123 + }, new Labels()); 124 + } 125 + 126 + async getLabelers( 127 + dids: string[], 128 + includeTakedowns = false, 129 + ): Promise<Labelers> { 130 + const uris = dids.map(labelerDidToUri); 131 + const res = await this.dataplane.records.getRecords(uris); 132 + return dids.reduce((acc, did, i) => { 133 + const record = parseRecord<LabelerRecord>( 134 + res.records[i], 135 + includeTakedowns, 136 + ); 137 + return acc.set(did, record ?? null); 138 + }, new HydrationMap<Labeler>()); 139 + } 140 + 141 + async getLabelerViewerStates( 142 + dids: string[], 143 + viewer: string, 144 + ): Promise<LabelerViewerStates> { 145 + const refs = dids.map((did) => ({ uri: labelerDidToUri(did) })); 146 + const likes = await this.dataplane.likes.byActorAndSubjects(viewer, refs); 147 + return dids.reduce((acc, did, i) => { 148 + return acc.set(did, { 149 + like: parseString(likes.uris[i]), 150 + }); 151 + }, new HydrationMap<LabelerViewerState>()); 152 + } 153 + 154 + async getLabelerAggregates( 155 + dids: string[], 156 + _viewer: string | null, 157 + ): Promise<LabelerAggs> { 158 + const refs = dids.map((did) => ({ uri: labelerDidToUri(did) })); 159 + const counts = await this.dataplane.interactions.getInteractionCounts(refs); 160 + return dids.reduce((acc, did, i) => { 161 + return acc.set(did, { 162 + likes: counts.likes[i] ?? 0, 163 + }); 164 + }, new HydrationMap<LabelerAgg>()); 165 + } 166 + } 167 + 168 + const labelerDidToUri = (did: string): string => { 169 + return AtUri.make(did, ids.SoSprkLabelerService, "self").toString(); 170 + }; 171 + 172 + const IMPERSONATION_LABEL = "impersonation"; 173 + const TAKEDOWN_LABELS = ["!takedown", "!suspend"]; 174 + const NEEDS_REVIEW_LABEL = "needs-review";
+10 -1
main.ts
··· 15 15 import { Views } from "./views/index.ts"; 16 16 import { AppContext, AppEnv } from "./context.ts"; 17 17 import { ServerConfig } from "./config.ts"; 18 + import { defaultLabelerHeader, parseLabelerHeader } from "./util.ts"; 18 19 19 20 await configureLogger(); 20 21 ··· 52 53 const idResolver = new IdResolver({ plcUrl: cfg.plcUrl }); 53 54 54 55 const dataplane = new DataPlane(db, idResolver); 55 - const hydrator = new Hydrator(dataplane); 56 + const hydrator = new Hydrator(dataplane, cfg.labelsFromIssuerDids); 56 57 const views = new Views({ 57 58 indexedAtEpoch: cfg.indexedAtEpoch, 58 59 videoCdn: cfg.videoCdn, ··· 67 68 adminPasses: cfg.adminPasswords, 68 69 }); 69 70 71 + const reqLabelers = (req: Request) => { 72 + const val = req.headers.get("atproto-accept-labelers") ?? undefined; 73 + const parsed = parseLabelerHeader(val); 74 + if (!parsed) return defaultLabelerHeader(cfg.labelsFromIssuerDids); 75 + return parsed; 76 + }; 77 + 70 78 const ctx = { 71 79 db, 72 80 dataplane, ··· 76 84 idResolver, 77 85 cfg, 78 86 authVerifier, 87 + reqLabelers, 79 88 }; 80 89 81 90 const app = createApp(ctx);
+14 -2
tests/util.ts
··· 12 12 import { Views } from "../views/index.ts"; 13 13 import { IdResolver } from "@atp/identity"; 14 14 import { ServerConfig, ServerConfigValues } from "../config.ts"; 15 + import { defaultLabelerHeader } from "../util.ts"; 15 16 16 17 // Configure mongodb-memory-server to use a specific download directory 17 18 // This prevents issues with empty paths when running with restricted permissions ··· 76 77 alternateAudienceDids: [], 77 78 bigThreadUris: new Set(["did:web:test"]), 78 79 maxThreadParents: 10, 80 + labelsFromIssuerDids: [], 79 81 }; 80 82 81 83 // ============================================================================ ··· 187 189 "CursorState", 188 190 models.cursorStateSchema, 189 191 ), 192 + Labeler: connection.model<models.LabelerDocument>( 193 + "Labeler", 194 + models.labelerSchema, 195 + ), 196 + Label: connection.model<models.LabelDocument>( 197 + "Label", 198 + models.labelSchema, 199 + ), 190 200 }; 191 201 192 202 // Seed data ··· 255 265 } as unknown as Database; 256 266 257 267 const dataplane = new DataPlane(mockDb, idResolver); 258 - const hydrator = new Hydrator(dataplane); 268 + const hydrator = new Hydrator(dataplane, cfg.labelsFromIssuerDids); 259 269 const views = new Views(cfg); 260 270 const authVerifier = createAuthVerifier(dataplane, { 261 271 ownDid: cfg.serverDid, ··· 273 283 idResolver, 274 284 cfg, 275 285 authVerifier, 286 + reqLabelers: () => defaultLabelerHeader(cfg.labelsFromIssuerDids), 276 287 }; 277 288 } 278 289 ··· 332 343 } as unknown as Database; 333 344 334 345 const dataplane = new DataPlane(db, idResolver); 335 - const hydrator = new Hydrator(dataplane); 346 + const hydrator = new Hydrator(dataplane, cfg.labelsFromIssuerDids); 336 347 const views = new Views(cfg); 337 348 const authVerifier = createAuthVerifier(dataplane, { 338 349 ownDid: cfg.serverDid, ··· 350 361 idResolver, 351 362 cfg, 352 363 authVerifier, 364 + reqLabelers: () => defaultLabelerHeader(cfg.labelsFromIssuerDids), 353 365 }; 354 366 355 367 const cleanup = async () => {
+46
util.ts
··· 1 + import { parseList } from "structured-headers"; 2 + 3 + export type ParsedLabelers = { 4 + dids: string[]; 5 + redact: Set<string>; 6 + }; 7 + 8 + export const parseLabelerHeader = ( 9 + header: string | undefined, 10 + ): ParsedLabelers | null => { 11 + // An empty header is valid, so we shouldn't return null 12 + // https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 13 + if (header === undefined) return null; 14 + const labelerDids = new Set<string>(); 15 + const redactDids = new Set<string>(); 16 + const parsed = parseList(header); 17 + for (const item of parsed) { 18 + const did = item[0].toString(); 19 + if (!did) { 20 + return null; 21 + } 22 + labelerDids.add(did); 23 + const redact = item[1].get("redact")?.valueOf(); 24 + if (redact === true) { 25 + redactDids.add(did); 26 + } 27 + } 28 + return { 29 + dids: [...labelerDids], 30 + redact: redactDids, 31 + }; 32 + }; 33 + 34 + export const defaultLabelerHeader = (dids: string[]): ParsedLabelers => { 35 + return { 36 + dids, 37 + redact: new Set(dids), 38 + }; 39 + }; 40 + 41 + export const formatLabelerHeader = (parsed: ParsedLabelers): string => { 42 + const parts = parsed.dids.map((did) => 43 + parsed.redact.has(did) ? `${did};redact` : did 44 + ); 45 + return parts.join(","); 46 + };