···11-# main indexers
22-JETSTREAM_URL="wss://jetstream1.us-east.bsky.network"
33-SPACEDUST_URL="wss://spacedust.whey.party"
44-55-# for backfill (useless if you just started the instance right now)
66-CONSTELLATION_URL="https://constellation.microcosm.blue"
77-# i dont actually know why i need this
88-SLINGSHOT_URL="https://slingshot.whey.party"
99-1010-# bools
1111-INDEX_SERVER_ENABLED=true
1212-INDEX_SERVER_INVITES_REQUIRED=true
1313-1414-VIEW_SERVER_ENABLED=true
1515-INDEX_SERVER_INVITES_REQUIRED=true
1616-1717-# this is for both index and view server btw
1818-SERVICE_DID="did:web:local3768forumtest.whey.party"
1919-SERVICE_ENDPOINT="https://local3768forumtest.whey.party"
2020-SERVER_PORT="3768"
···11+{
22+ // Main indexers
33+ "jetstream": "wss://jetstream1.us-east.bsky.network", // you can self host it -> https://github.com/bluesky-social/jetstream
44+ "spacedust": "wss://spacedust.your.site", // you can self host it -> https://www.microcosm.blue
55+66+ // For backfill (optional)
77+ "constellation": "https://constellation.microcosm.blue", // (not useful on a new setup — requires pre-existing data to backfill)
88+99+ // Utility services
1010+ "slingshot": "https://slingshot.your.site", // you can self host it -> https://www.microcosm.blue
1111+1212+ // Index Server config
1313+ "indexServer": {
1414+ "inviteOnly": true,
1515+ "port": 3767,
1616+ "did": "did:web:skyliteindexserver.your.site", // should be the same domain as the endpoint
1717+ "host": "https://skyliteindexserver.your.site"
1818+ },
1919+2020+ // View Server config
2121+ "viewServer": {
2222+ "inviteOnly": true,
2323+ "port": 3768,
2424+ "did": "did:web:skyliteviewserver.your.site", // should be the same domain as the endpoint
2525+ "host": "https://skyliteviewserver.your.site",
2626+2727+ // In order of which skylite index servers or bsky appviews to use first
2828+ "indexPriority": [
2929+ "user#skylite_index", // user resolved skylite index server
3030+ "did:web:backupindexserver.your.site#skylite_index", // a specific skylite index server
3131+ "user#bsky_appview", // user resolved bsky appview
3232+ "did:web:api.bsky.app#bsky_appview" // a specific bsky appview
3333+ ]
3434+ }
3535+}
+45
config.ts
···11+import { parse } from "jsr:@std/jsonc";
22+import * as z from "npm:zod";
33+44+// configure these from the config.jsonc file (you can use config.jsonc.example as reference)
55+const indexTarget = z.string().refine(
66+ (val) => {
77+ const parts = val.split("#");
88+ if (parts.length !== 2) return false;
99+1010+ const [prefix, suffix] = parts;
1111+ const validPrefix = prefix === "user" || prefix.startsWith("did:web:");
1212+ const validSuffix = suffix === "skylite_index" || suffix === "bsky_appview";
1313+1414+ return validPrefix && validSuffix;
1515+ },
1616+ {
1717+ message:
1818+ "Each indexPriority entry must be in the form 'user#skylite_index', 'user#bsky_appview', 'did:web:...#skylite_index', or 'did:web:...#bsky_appview'",
1919+ }
2020+);
2121+2222+const ConfigSchema = z.object({
2323+ jetstream: z.string(),
2424+ spacedust: z.string(),
2525+ constellation: z.string(),
2626+ slingshot: z.string(),
2727+ indexServer: z.object({
2828+ inviteOnly: z.boolean(),
2929+ port: z.number(),
3030+ did: z.string(),
3131+ host: z.string(),
3232+ }),
3333+ viewServer: z.object({
3434+ inviteOnly: z.boolean(),
3535+ port: z.number(),
3636+ did: z.string(),
3737+ host: z.string(),
3838+ indexPriority: z.array(indexTarget),
3939+ }),
4040+});
4141+4242+const raw = await Deno.readTextFile("config.jsonc");
4343+const config = ConfigSchema.parse(parse(raw));
4444+4545+export { config };
+2-1
deno.json
···11{
22 "tasks": {
33- "dev": "deno run --watch -A --env-file main.ts"
33+ "index": "deno run --watch -A --env-file main-index.ts",
44+ "view": "deno run --watch -A --env-file main-view.ts"
45 },
56 "imports": {
67 "@std/assert": "jsr:@std/assert@1"
+2-2
index/jetstream.ts
···11import { Database } from "jsr:@db/sqlite@0.11";
22-import { handleIndex } from "../main.ts";
22+import { config } from "../config.ts";
33import { resolveRecordFromURI } from "../utils/records.ts";
44import { JetstreamManager } from "../utils/sharders.ts";
55···2929 });
3030}
31313232-export async function handleJetstream(msg: any) {
3232+export async function handleJetstream(msg: any, handleIndex: Function) {
3333 console.log("Received Jetstream message: ", msg);
34343535 const op = msg.commit.operation;
···11-import { setupAuth, getAuthenticatedDid, authVerifier } from "./utils/auth.ts";
22-import { JetstreamManager, SpacedustManager } from "./utils/sharders.ts";
33-import { resolveRecordFromURI, validateRecord } from "./utils/records.ts";
44-import { setupSystemDb } from "./utils/dbsystem.ts";
55-import { setupUserDb } from "./utils/dbuser.ts";
66-import { handleSpacedust, startSpacedust } from "./index/spacedust.ts";
77-import { handleJetstream, startJetstream } from "./index/jetstream.ts";
88-import { Database } from "jsr:@db/sqlite@0.11";
99-//import express from "npm:express";
1010-//import { createServer } from "./xrpc/index.ts";
1111-import { indexHandlerContext } from "./index/types.ts";
1212-import * as IndexServerTypes from "./utils/indexservertypes.ts";
1313-import * as ViewServerTypes from "./utils/viewservertypes.ts";
1414-import * as ATPAPI from "npm:@atproto/api";
1515-import { didDocument } from "./utils/diddoc.ts";
1616-import { cachedFetch, searchParamsToJson, withCors } from "./utils/server.ts";
1717-import { IndexServer, IndexServerConfig } from "./indexserver.ts"
1818-import { viewServerHandler } from "./viewserver.ts";
1919-2020-export const jetstreamurl = Deno.env.get("JETSTREAM_URL");
2121-export const slingshoturl = Deno.env.get("SLINGSHOT_URL");
2222-export const constellationurl = Deno.env.get("CONSTELLATION_URL");
2323-export const spacedusturl = Deno.env.get("SPACEDUST_URL");
2424-2525-// ------------------------------------------
2626-// AppView Setup
2727-// ------------------------------------------
2828-2929-const config: IndexServerConfig = {
3030- baseDbPath: './dbs', // The directory for user databases
3131- systemDbPath: './system.db', // The path for the main system database
3232- jetstreamUrl: jetstreamurl || ""
3333-};
3434-const registeredUsersIndexServer = new IndexServer(config);
3535-setupSystemDb(registeredUsersIndexServer.systemDB);
3636-3737-// add me lol
3838-registeredUsersIndexServer.systemDB.exec(`
3939- INSERT OR IGNORE INTO users (did, role, registrationdate, onboardingstatus)
4040- VALUES (
4141- 'did:plc:mn45tewwnse5btfftvd3powc',
4242- 'admin',
4343- datetime('now'),
4444- 'ready'
4545- );
4646-4747- INSERT OR IGNORE INTO users (did, role, registrationdate, onboardingstatus)
4848- VALUES (
4949- 'did:web:did12.whey.party',
5050- 'admin',
5151- datetime('now'),
5252- 'ready'
5353- );
5454-`)
5555-5656-registeredUsersIndexServer.start();
5757-5858-// should do both of these per user actually, since now each user has their own db
5959-// also the set of records and backlinks to listen should be seperate between index and view servers
6060-// damn
6161-// export const spacedustManager = new SpacedustManager((msg) =>
6262-// handleSpacedust(msg)
6363-// );
6464-// export const jetstreamManager = new JetstreamManager((msg) =>
6565-// handleJetstream(msg)
6666-// );
6767-// startSpacedust();
6868-// startJetstream();
6969-7070-// 1. connect to system db
7171-// 2. get all registered users
7272-// parse config (maybe some are only indexes and maybe some are only views)
7373-// map all new jetstream and spacedust listeners
7474-// call handleIndex with the specific db to use
7575-7676-setupAuth({
7777- // local3768forumtest is just my tunnel from my dev env to the outside web that im reusing from forumtest
7878- serviceDid: `${Deno.env.get("SERVICE_DID")}`,
7979- //keyCacheSize: 500,
8080- //keyCacheTTL: 10 * 60 * 1000,
8181-});
8282-8383-// ------------------------------------------
8484-// XRPC Method Implementations
8585-// ------------------------------------------
8686-8787-const indexServerRoutes = new Set([
8888- "/xrpc/app.bsky.actor.getProfile",
8989- "/xrpc/app.bsky.actor.getProfiles",
9090- "/xrpc/app.bsky.feed.getActorFeeds",
9191- "/xrpc/app.bsky.feed.getFeedGenerator",
9292- "/xrpc/app.bsky.feed.getFeedGenerators",
9393- "/xrpc/app.bsky.feed.getPosts",
9494- "/xrpc/party.whey.app.bsky.feed.getActorLikesPartial",
9595- "/xrpc/party.whey.app.bsky.feed.getAuthorFeedPartial",
9696- "/xrpc/party.whey.app.bsky.feed.getLikesPartial",
9797- "/xrpc/party.whey.app.bsky.feed.getPostThreadPartial",
9898- "/xrpc/party.whey.app.bsky.feed.getQuotesPartial",
9999- "/xrpc/party.whey.app.bsky.feed.getRepostedByPartial",
100100- // more federated endpoints, not planned yet, lexicons will come later
101101- /*
102102- app.bsky.graph.getLists // doesnt need to because theres no items[], and its self ProfileViewBasic
103103- app.bsky.graph.getList // needs to be Partial-ed (items[] union with ProfileViewRef)
104104- app.bsky.graph.getActorStarterPacks // maybe doesnt need to be Partial-ed because its self ProfileViewBasic
105105-106106- app.bsky.feed.getListFeed // uhh actually already exists its getListFeedPartial
107107- */
108108- "/xrpc/party.whey.app.bsky.feed.getListFeedPartial",
109109-]);
110110-111111-Deno.serve(
112112- { port: Number(`${Deno.env.get("SERVER_PORT")}`) },
113113- async (req: Request): Promise<Response> => {
114114- const url = new URL(req.url);
115115- const pathname = url.pathname;
116116- // const searchParams = searchParamsToJson(url.searchParams);
117117- // let reqBody: undefined | string;
118118- // let jsonbody: undefined | Record<string, unknown>;
119119- // try {
120120- // const clone = req.clone();
121121- // jsonbody = await clone.json();
122122- // } catch (e) {
123123- // console.warn("Request body is not valid JSON:", e);
124124- // }
125125- if (pathname === "/.well-known/did.json") {
126126- return new Response(JSON.stringify(didDocument), {
127127- headers: withCors({ "Content-Type": "application/json" }),
128128- });
129129- }
130130- if (pathname === "/health") {
131131- return new Response("OK", {
132132- status: 200,
133133- headers: withCors({
134134- "Content-Type": "text/plain",
135135- }),
136136- });
137137- }
138138- if (req.method === "OPTIONS") {
139139- return new Response(null, {
140140- status: 204,
141141- headers: {
142142- "Access-Control-Allow-Origin": "*",
143143- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
144144- "Access-Control-Allow-Headers": "*",
145145- },
146146- });
147147- }
148148- // const bskyUrl = `https://api.bsky.app${pathname}${url.search}`;
149149- // const hasAuth = req.headers.has("authorization");
150150- // const xrpcMethod = pathname.startsWith("/xrpc/")
151151- // ? pathname.slice("/xrpc/".length)
152152- // : null;
153153- console.log(`request for "${pathname}"`)
154154- const constellation = pathname.startsWith("/links")
155155-156156- // return await viewServerHandler(req)
157157-158158- if (constellation) {
159159- return registeredUsersIndexServer.constellationAPIHandler(req);
160160- }
161161-162162- if (indexServerRoutes.has(pathname)) {
163163- return registeredUsersIndexServer.indexServerHandler(req);
164164- } else {
165165- return await viewServerHandler(req);
166166- }
167167- }
168168-);
169169-170170-// ------------------------------------------
171171-// Indexer
172172-// ------------------------------------------
+2-2
readme.md
···4444this project is pre-alpha and not intended for general use yet. you are welcome to experiment if you dont mind errors or breaking changes.
45454646the project is split into two, the "Index Server" and the "View Server".
4747-these currently run in a single process and share the same HTTP server and port.
4747+despite both living in this repo, they run different http servers with different configs
48484949-configuration is in the `.env` file
4949+example configuration is in the `config.jsonc.example` file
50505151expose your localhost to the web using a tunnel or something and use that url as the custom appview url
5252
···1122import { DidResolver, HandleResolver } from "npm:@atproto/identity";
33import { Database } from "jsr:@db/sqlite@0.11";
44+import { AtUri } from "npm:@atproto/api";
45const systemDB = new Database("./system.db") // TODO: temporary shim. should seperate this to its own central system db instead of the now instantiated system dbs
56type DidMethod = "web" | "plc";
67type DidDoc = {
···224225 } catch (err) {
225226 console.error(`Failed to extract/store PDS and handle for '${did}':`, err);
226227 return null;
228228+ }
229229+}
230230+231231+export function extractDid(input: string): string {
232232+ if (input.startsWith('did:')) {
233233+ return input
234234+ }
235235+236236+ try {
237237+ const uri = new AtUri(input)
238238+ return uri.host
239239+ } catch (e) {
240240+ throw new Error(`Invalid input: expected a DID or a valid AT URI, got "${input}"`)
227241 }
228242}
+4-4
utils/server.ts
···11import ky from "npm:ky";
22import QuickLRU from "npm:quick-lru";
33import { createHash } from "node:crypto";
44-import { slingshoturl, constellationurl } from "../main.ts";
44+import { config } from "../config.ts";
55import * as ATPAPI from "npm:@atproto/api";
6677const cache = new QuickLRU({ maxSize: 10000 });
···5757export async function resolveIdentity(
5858 actor: string
5959): Promise<SlingshotMiniDoc> {
6060- const url = `${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${actor}`;
6060+ const url = `${config.slingshot}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${actor}`;
6161 return (await cachedFetch(url)) as SlingshotMiniDoc;
6262}
6363export async function getRecord({
···155155 collection: string,
156156 rkey: string
157157): Promise<GetRecord> {
158158- const url = `${slingshoturl}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=${collection}&rkey=${rkey}`;
158158+ const url = `${config.slingshot}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=${collection}&rkey=${rkey}`;
159159 const result = (await cachedFetch(url)) as GetRecord;
160160 return result as GetRecord;
161161}
···169169 collection: string;
170170 path: string;
171171}): Promise<number> {
172172- const url = `${constellationurl}/links/count/distinct-dids?target=${did}&collection=${collection}&path=${path}`;
172172+ const url = `${config.constellation}/links/count/distinct-dids?target=${did}&collection=${collection}&path=${path}`;
173173 const result = (await cachedFetch(url)) as ConstellationDistinctDids;
174174 return result.total;
175175}