Live video on the AT Protocol
1import fs from "node:fs/promises";
2import os from "node:os";
3import path from "node:path";
4
5import { Secp256k1Keypair, randomStr } from "@atproto/crypto";
6import * as pds from "@atproto/pds";
7
8import getPort from "get-port";
9import * as ui8 from "uint8arrays";
10
11import { ADMIN_PASSWORD, JWT_SECRET } from "./constants.js";
12
13export interface PdsServerOptions extends Partial<pds.ServerEnvironment> {
14 didPlcUrl: string;
15}
16
17export interface AdditionalPdsContext {
18 dataDirectory: string;
19 blobstoreLoc: string;
20}
21
22export class TestPdsServer {
23 constructor(
24 public readonly server: pds.PDS,
25 public readonly url: string,
26 public readonly port: number,
27 public readonly additional: AdditionalPdsContext,
28 ) {}
29
30 static async create(config: PdsServerOptions): Promise<TestPdsServer> {
31 const plcRotationKey = await Secp256k1Keypair.create({ exportable: true });
32 const plcRotationPriv = ui8.toString(await plcRotationKey.export(), "hex");
33 const recoveryKey = (await Secp256k1Keypair.create()).did();
34
35 const port = config.port || (await getPort());
36 const url = `http://localhost:${port}`;
37
38 const blobstoreLoc = path.join(os.tmpdir(), randomStr(8, "base32"));
39 const dataDirectory = path.join(os.tmpdir(), randomStr(8, "base32"));
40
41 await fs.mkdir(dataDirectory, { recursive: true });
42
43 const env: pds.ServerEnvironment = {
44 devMode: true,
45 port,
46 dataDirectory: dataDirectory,
47 blobstoreDiskLocation: blobstoreLoc,
48 recoveryDidKey: recoveryKey,
49 adminPassword: ADMIN_PASSWORD,
50 jwtSecret: JWT_SECRET,
51 serviceHandleDomains: [".test"],
52 bskyAppViewUrl: "https://appview.invalid",
53 bskyAppViewDid: "did:example:invalid",
54 bskyAppViewCdnUrlPattern: "http://cdn.appview.com/%s/%s/%s",
55 modServiceUrl: "https://moderator.invalid",
56 modServiceDid: "did:example:invalid",
57 plcRotationKeyK256PrivateKeyHex: plcRotationPriv,
58 inviteRequired: false,
59 disableSsrfProtection: true,
60 serviceName: "Development PDS",
61 // brandColor: "#ffcb1e",
62 errorColor: undefined,
63 logoUrl:
64 "https://uxwing.com/wp-content/themes/uxwing/download/animals-and-birds/bee-icon.png",
65 homeUrl: "https://bsky.social/",
66 termsOfServiceUrl: "https://bsky.social/about/support/tos",
67 privacyPolicyUrl: "https://bsky.social/about/support/privacy-policy",
68 supportUrl: "https://blueskyweb.zendesk.com/hc/en-us",
69 ...config,
70 };
71
72 const cfg = pds.envToCfg(env);
73 const secrets = pds.envToSecrets(env);
74
75 const server = await pds.PDS.create(cfg, secrets);
76
77 await server.start();
78
79 return new TestPdsServer(server, url, port, {
80 dataDirectory: dataDirectory,
81 blobstoreLoc: blobstoreLoc,
82 });
83 }
84
85 get ctx(): pds.AppContext {
86 return this.server.ctx;
87 }
88
89 adminAuth(): string {
90 return (
91 "Basic " +
92 ui8.toString(
93 ui8.fromString(`admin:${ADMIN_PASSWORD}`, "utf8"),
94 "base64pad",
95 )
96 );
97 }
98
99 adminAuthHeaders() {
100 return {
101 authorization: this.adminAuth(),
102 };
103 }
104
105 jwtSecretKey() {
106 return pds.createSecretKeyObject(JWT_SECRET);
107 }
108
109 async processAll() {
110 await this.ctx.backgroundQueue.processAll();
111 }
112
113 async close() {
114 await this.server.destroy();
115
116 await fs.rm(this.additional.dataDirectory, {
117 recursive: true,
118 force: true,
119 });
120 await fs.rm(this.additional.blobstoreLoc, { force: true });
121 }
122}