chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
#!/usr/bin/env node
/**
* git-host: a git forge served from a PDS reached over the wire.
*
* git-host --pds https://pds.example.com --account alice.example.com
*
* The PDS stays the data store. This process serves git smart HTTP, the
* file browser, the dev.pdsjs.git.* methods and the browser app, indexes
* collaboration records off Jetstream, and catches up from the archive on
* boot when replay is on.
*/
import { mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { parseArgs } from 'node:util';
import { zstdDecompressSync } from 'node:zlib';
import { backfill, watch } from '@pdsjs/appview';
import { createAppViewStore } from '@pdsjs/appview/store';
import {
GIT_ISSUE_COLLECTION,
GIT_PULL_COLLECTION,
GIT_REPO_COLLECTION,
GIT_REVIEW_COLLECTION,
} from '@pdsjs/git';
import { createForgeAuth } from './auth.js';
import { requestContext } from './context.js';
import { createRemoteAccount } from './remote.js';
import { createGitHostServer } from './server.js';
/** The collections the forge's reads are about. */
const WATCHED_COLLECTIONS = [
GIT_REPO_COLLECTION,
GIT_PULL_COLLECTION,
GIT_REVIEW_COLLECTION,
GIT_ISSUE_COLLECTION,
'dev.pdsjs.git.offer',
];
/**
* The SQLite driver, whichever is installed: better-sqlite3 first, node's
* own module otherwise (node 22.5 and up).
* @param {string} path
* @returns {Promise<import('@pdsjs/storage-sqlite/driver').SQLiteDatabase>}
*/
async function openDatabase(path) {
try {
const { default: Database } = await import('better-sqlite3');
return new Database(path);
} catch {
const { DatabaseSync } = await import('node:sqlite');
return new DatabaseSync(path);
}
}
const { values } = parseArgs({
options: {
pds: { type: 'string' },
account: { type: 'string' },
db: { type: 'string', default: 'git-appview.sqlite' },
port: { type: 'string', default: '8046' },
origin: { type: 'string' },
ui: { type: 'string' },
'plc-url': { type: 'string', default: 'https://plc.directory' },
jetstream: {
type: 'string',
default: 'wss://jetstream.us-east.bsky.network',
},
'jetstream-api-key': {
type: 'string',
default: process.env.JETSTREAM_API_KEY,
},
replay: { type: 'boolean', default: false },
'list-service': { type: 'string', default: 'https://bsky.network' },
'no-watch': { type: 'boolean', default: false },
'no-backfill': { type: 'boolean', default: false },
help: { type: 'boolean', default: false },
},
});
if (values.help || !values.pds || !values.account) {
console.log(`usage: git-host --pds <url> --account <handle-or-did> [options]
--db <path> the index database (default ./git-appview.sqlite)
--port <n> the port to serve on (default 8046)
--origin <url> the origin requests are read as carrying
--ui <dir> the browser app's build, served at /
--plc-url <url> the directory DIDs resolve against
--jetstream <url> the jetstream to index from (default a v2 instance)
--jetstream-api-key the archive's bearer token, for replay on a metered
instance; also read from JETSTREAM_API_KEY
--replay catch up from the jetstream archive on boot instead
of walking the relay; implies --no-backfill
--list-service <url> the relay the backfill lists repos from
--no-watch skip the jetstream tail
--no-backfill skip the first-boot historical walk
`);
process.exit(values.help ? 0 : 1);
}
const notice = (/** @type {string} */ message) =>
console.error(`[git-host] ${message}`);
const dbPath = join(values.db);
mkdirSync(dirname(dbPath), { recursive: true });
const db = await openDatabase(dbPath);
const store = createAppViewStore(db);
const account = createRemoteAccount({
service: values.pds,
account: values.account,
// Writes carry the requesting account's own token, which the dispatch
// placed in this request's context.
getToken: async () => requestContext.getStore()?.token ?? null,
});
// The login round trip needs a stable origin: the client_id names it. An
// origin-less deployment serves reads only.
const auth = values.origin
? createForgeAuth({
origin: values.origin,
accountDid: await account.getDid(),
service: values.pds,
plcUrl: values['plc-url'],
onNotice: notice,
})
: undefined;
const host = createGitHostServer({
account,
store,
auth,
origin: values.origin,
uiDir: values.ui,
plcUrl: values['plc-url'],
onNotice: notice,
});
await host.listen(Number(values.port));
notice(`serving ${values.account} from ${values.pds} on :${values.port}`);
const controller = new AbortController();
for (const signal of ['SIGINT', 'SIGTERM']) {
process.on(signal, () => {
controller.abort();
void host.close().then(() => process.exit(0));
});
}
// Replay replaces the relay walk: the archive is the same history, asked
// of the jetstream itself instead of every account's PDS.
const replay = values.replay || Boolean(values['jetstream-api-key']);
if (!values['no-backfill'] && !replay) {
void backfill({
store,
collections: WATCHED_COLLECTIONS,
listService: values['list-service'],
plcUrl: values['plc-url'],
signal: controller.signal,
onNotice: notice,
});
}
if (!values['no-watch']) {
void watch({
store,
service: values.jetstream,
collections: WATCHED_COLLECTIONS,
replay: replay
? {
service: values.jetstream,
apiKey: values['jetstream-api-key'],
decompress: async (bytes) =>
new Uint8Array(zstdDecompressSync(Buffer.from(bytes))),
}
: undefined,
signal: controller.signal,
onNotice: notice,
});
}