import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { Secp256k1Keypair, randomStr } from "@atproto/crypto"; import * as pds from "@atproto/pds"; import getPort from "get-port"; import * as ui8 from "uint8arrays"; import { ADMIN_PASSWORD, JWT_SECRET } from "./constants.js"; export interface PdsServerOptions extends Partial { didPlcUrl: string; } export interface AdditionalPdsContext { dataDirectory: string; blobstoreLoc: string; } export class TestPdsServer { constructor( public readonly server: pds.PDS, public readonly url: string, public readonly port: number, public readonly additional: AdditionalPdsContext, ) {} static async create(config: PdsServerOptions): Promise { const plcRotationKey = await Secp256k1Keypair.create({ exportable: true }); const plcRotationPriv = ui8.toString(await plcRotationKey.export(), "hex"); const recoveryKey = (await Secp256k1Keypair.create()).did(); const port = config.port || (await getPort()); const url = `http://localhost:${port}`; const blobstoreLoc = path.join(os.tmpdir(), randomStr(8, "base32")); const dataDirectory = path.join(os.tmpdir(), randomStr(8, "base32")); await fs.mkdir(dataDirectory, { recursive: true }); const env: pds.ServerEnvironment = { devMode: true, port, dataDirectory: dataDirectory, blobstoreDiskLocation: blobstoreLoc, recoveryDidKey: recoveryKey, adminPassword: ADMIN_PASSWORD, jwtSecret: JWT_SECRET, serviceHandleDomains: [".test"], bskyAppViewUrl: "https://appview.invalid", bskyAppViewDid: "did:example:invalid", bskyAppViewCdnUrlPattern: "http://cdn.appview.com/%s/%s/%s", modServiceUrl: "https://moderator.invalid", modServiceDid: "did:example:invalid", plcRotationKeyK256PrivateKeyHex: plcRotationPriv, inviteRequired: false, disableSsrfProtection: true, serviceName: "Development PDS", // brandColor: "#ffcb1e", errorColor: undefined, logoUrl: "https://uxwing.com/wp-content/themes/uxwing/download/animals-and-birds/bee-icon.png", homeUrl: "https://bsky.social/", termsOfServiceUrl: "https://bsky.social/about/support/tos", privacyPolicyUrl: "https://bsky.social/about/support/privacy-policy", supportUrl: "https://blueskyweb.zendesk.com/hc/en-us", ...config, }; const cfg = pds.envToCfg(env); const secrets = pds.envToSecrets(env); const server = await pds.PDS.create(cfg, secrets); await server.start(); return new TestPdsServer(server, url, port, { dataDirectory: dataDirectory, blobstoreLoc: blobstoreLoc, }); } get ctx(): pds.AppContext { return this.server.ctx; } adminAuth(): string { return ( "Basic " + ui8.toString( ui8.fromString(`admin:${ADMIN_PASSWORD}`, "utf8"), "base64pad", ) ); } adminAuthHeaders() { return { authorization: this.adminAuth(), }; } jwtSecretKey() { return pds.createSecretKeyObject(JWT_SECRET); } async processAll() { await this.ctx.backgroundQueue.processAll(); } async close() { await this.server.destroy(); await fs.rm(this.additional.dataDirectory, { recursive: true, force: true, }); await fs.rm(this.additional.blobstoreLoc, { force: true }); } }