atproto pds in zig
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361const std = @import("std");
pub const Options = struct { host: []const u8 = "127.0.0.1", port: u16 = 2583, db_path: []const u8 = "dev/zds.sqlite3", public_url: ?[]const u8 = null, server_did: ?[]const u8 = null, plc_directory: ?[]const u8 = null, plc_rotation_key: ?[]const u8 = null, recovery_did_key: ?[]const u8 = null, mail_provider: ?[]const u8 = null, email_from: ?[]const u8 = null, resend_api_key: ?[]const u8 = null, comail_api_key: ?[]const u8 = null, comail_did: ?[]const u8 = null, blob_upload_limit: ?usize = null, blobstore_path: ?[]const u8 = null, handle_domains: ?[]const u8 = null, crawlers: ?[]const u8 = null, max_concurrent_repo_exports: ?usize = null, proxy_service_did: ?[]const u8 = null, proxy_service_id: ?[]const u8 = null, proxy_service_url: ?[]const u8 = null, jwt_secret: ?[]const u8 = null, dpop_secret: ?[]const u8 = null, admin_token: ?[]const u8 = null, invite_required: bool = false, permissioned_data: bool = false, dev_tools: bool = false, log_level: ?[]const u8 = null,};
pub const ParseError = error{ Help, InvalidLogLevel, MissingBlobUploadLimit, MissingBlobstorePath, MissingCrawlers, MissingMaxConcurrentRepoExports, MissingProxyServiceDid, MissingProxyServiceId, MissingProxyServiceUrl, MissingDatabasePath, MissingEmailFrom, MissingHandleDomains, MissingHost, MissingAdminToken, MissingJwtSecret, MissingDpopSecret, MissingLogLevel, MissingPlcDirectory, MissingPlcRotationKey, MissingRecoveryDidKey, MissingMailProvider, MissingPort, MissingPublicUrl, MissingResendApiKey, MissingComailApiKey, MissingComailDid, MissingServerDid, UnknownArgument,} || std.fmt.ParseIntError;
pub fn parse(init: std.process.Init.Minimal) ParseError!Options { var options = Options{ .host = env("ZDS_HOST") orelse "127.0.0.1", .port = try envU16("ZDS_PORT") orelse 2583, .db_path = env("ZDS_DB") orelse env("ZDS_DB_PATH") orelse "dev/zds.sqlite3", .public_url = env("ZDS_PUBLIC_URL"), .server_did = env("ZDS_SERVER_DID"), .plc_directory = env("ZDS_PLC_DIRECTORY"), .plc_rotation_key = env("ZDS_PLC_ROTATION_KEY"), .recovery_did_key = env("ZDS_RECOVERY_DID_KEY"), .mail_provider = env("ZDS_MAIL_PROVIDER"), .email_from = env("ZDS_EMAIL_FROM"), .resend_api_key = env("ZDS_RESEND_API_KEY"), .comail_api_key = env("ZDS_COMAIL_API_KEY"), .comail_did = env("ZDS_COMAIL_DID"), .blob_upload_limit = try envUsize("ZDS_BLOB_UPLOAD_LIMIT"), .blobstore_path = env("ZDS_BLOBSTORE_PATH"), .handle_domains = env("ZDS_HANDLE_DOMAINS"), .crawlers = env("ZDS_CRAWLERS"), .max_concurrent_repo_exports = try envUsize("ZDS_MAX_CONCURRENT_REPO_EXPORTS"), .proxy_service_did = env("ZDS_PROXY_SERVICE_DID"), .proxy_service_id = env("ZDS_PROXY_SERVICE_ID"), .proxy_service_url = env("ZDS_PROXY_SERVICE_URL"), .jwt_secret = env("ZDS_JWT_SECRET"), .dpop_secret = env("ZDS_DPOP_SECRET"), .admin_token = env("ZDS_ADMIN_TOKEN"), .invite_required = envBool("ZDS_INVITE_REQUIRED"), .permissioned_data = envBool("ZDS_PERMISSIONED_DATA"), .dev_tools = envBool("ZDS_DEV_TOOLS"), .log_level = env("ZDS_LOG_LEVEL"), }; if (envBool("ZDS_DEBUG")) options.log_level = "debug";
var args = std.process.Args.Iterator.init(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) return error.Help; if (try parseSplitArg(&options, arg, &args)) continue; if (try parseJoinedArg(&options, arg)) continue; return error.UnknownArgument; }
return options;}
pub fn usage() void { std.debug.print( \\usage: zds [--host HOST] [--port PORT] [--db PATH] [--public-url URL] [--server-did DID] \\ [--plc-rotation-key KEY] [--recovery-did-key DIDKEY] \\ [--mail-provider comail|resend] [--email-from ADDRESS] \\ [--blob-upload-limit BYTES] [--blobstore-path PATH] \\ [--handle-domains DOMAINS] [--crawlers URLS] \\ [--max-concurrent-repo-exports N] [--jwt-secret SECRET] \\ [--dpop-secret SECRET] \\ [--proxy-service-did DID] [--proxy-service-id ID] [--proxy-service-url URL] \\ [--admin-token TOKEN] [--invite-required] \\ [--log-level error|info|debug] [--debug] \\ \\Runs a local PDS-shaped HTTP server. \\Environment defaults use ZDS_HOST, ZDS_PORT, and ZDS_DB. \\DOMAINS is a comma-separated list of leading-dot suffixes such as ".pds.example.com". \\URLS is a comma-separated crawler list, defaulting to bsky.network and vsky.network. \\ , .{});}
fn parseSplitArg(options: *Options, arg: []const u8, args: *std.process.Args.Iterator) ParseError!bool { if (std.mem.eql(u8, arg, "--debug")) { options.log_level = "debug"; return true; } if (std.mem.eql(u8, arg, "--invite-required")) { options.invite_required = true; return true; } if (std.mem.eql(u8, arg, "--port")) { options.port = try std.fmt.parseInt(u16, args.next() orelse return error.MissingPort, 10); return true; } if (std.mem.eql(u8, arg, "--host")) { options.host = args.next() orelse return error.MissingHost; return true; } if (std.mem.eql(u8, arg, "--db")) { options.db_path = args.next() orelse return error.MissingDatabasePath; return true; } if (std.mem.eql(u8, arg, "--public-url")) { options.public_url = args.next() orelse return error.MissingPublicUrl; return true; } if (std.mem.eql(u8, arg, "--server-did")) { options.server_did = args.next() orelse return error.MissingServerDid; return true; } if (std.mem.eql(u8, arg, "--plc-directory")) { options.plc_directory = args.next() orelse return error.MissingPlcDirectory; return true; } if (std.mem.eql(u8, arg, "--plc-rotation-key")) { options.plc_rotation_key = args.next() orelse return error.MissingPlcRotationKey; return true; } if (std.mem.eql(u8, arg, "--recovery-did-key")) { options.recovery_did_key = args.next() orelse return error.MissingRecoveryDidKey; return true; } if (std.mem.eql(u8, arg, "--mail-provider")) { options.mail_provider = args.next() orelse return error.MissingMailProvider; return true; } if (std.mem.eql(u8, arg, "--email-from")) { options.email_from = args.next() orelse return error.MissingEmailFrom; return true; } if (std.mem.eql(u8, arg, "--resend-api-key")) { options.resend_api_key = args.next() orelse return error.MissingResendApiKey; return true; } if (std.mem.eql(u8, arg, "--comail-api-key")) { options.comail_api_key = args.next() orelse return error.MissingComailApiKey; return true; } if (std.mem.eql(u8, arg, "--comail-did")) { options.comail_did = args.next() orelse return error.MissingComailDid; return true; } if (std.mem.eql(u8, arg, "--blob-upload-limit")) { options.blob_upload_limit = try std.fmt.parseInt(usize, args.next() orelse return error.MissingBlobUploadLimit, 10); return true; } if (std.mem.eql(u8, arg, "--blobstore-path")) { options.blobstore_path = args.next() orelse return error.MissingBlobstorePath; return true; } if (std.mem.eql(u8, arg, "--handle-domains")) { options.handle_domains = args.next() orelse return error.MissingHandleDomains; return true; } if (std.mem.eql(u8, arg, "--crawlers")) { options.crawlers = args.next() orelse return error.MissingCrawlers; return true; } if (std.mem.eql(u8, arg, "--max-concurrent-repo-exports")) { options.max_concurrent_repo_exports = try std.fmt.parseInt(usize, args.next() orelse return error.MissingMaxConcurrentRepoExports, 10); return true; } if (std.mem.eql(u8, arg, "--proxy-service-did")) { options.proxy_service_did = args.next() orelse return error.MissingProxyServiceDid; return true; } if (std.mem.eql(u8, arg, "--proxy-service-id")) { options.proxy_service_id = args.next() orelse return error.MissingProxyServiceId; return true; } if (std.mem.eql(u8, arg, "--proxy-service-url")) { options.proxy_service_url = args.next() orelse return error.MissingProxyServiceUrl; return true; } if (std.mem.eql(u8, arg, "--jwt-secret")) { options.jwt_secret = args.next() orelse return error.MissingJwtSecret; return true; } if (std.mem.eql(u8, arg, "--dpop-secret")) { options.dpop_secret = args.next() orelse return error.MissingDpopSecret; return true; } if (std.mem.eql(u8, arg, "--admin-token")) { options.admin_token = args.next() orelse return error.MissingAdminToken; return true; } if (std.mem.eql(u8, arg, "--log-level")) { options.log_level = args.next() orelse return error.MissingLogLevel; return true; } return false;}
fn parseJoinedArg(options: *Options, arg: []const u8) ParseError!bool { inline for (joined_string_options) |option| { if (std.mem.startsWith(u8, arg, option.flag)) { @field(options, option.field) = arg[option.flag.len..]; return true; } } if (std.mem.startsWith(u8, arg, "--port=")) { options.port = try std.fmt.parseInt(u16, arg["--port=".len..], 10); return true; } if (std.mem.startsWith(u8, arg, "--blob-upload-limit=")) { options.blob_upload_limit = try std.fmt.parseInt(usize, arg["--blob-upload-limit=".len..], 10); return true; } if (std.mem.startsWith(u8, arg, "--max-concurrent-repo-exports=")) { options.max_concurrent_repo_exports = try std.fmt.parseInt(usize, arg["--max-concurrent-repo-exports=".len..], 10); return true; } return false;}
const JoinedStringOption = struct { flag: []const u8, field: []const u8,};
const joined_string_options = [_]JoinedStringOption{ .{ .flag = "--host=", .field = "host" }, .{ .flag = "--db=", .field = "db_path" }, .{ .flag = "--public-url=", .field = "public_url" }, .{ .flag = "--server-did=", .field = "server_did" }, .{ .flag = "--plc-directory=", .field = "plc_directory" }, .{ .flag = "--plc-rotation-key=", .field = "plc_rotation_key" }, .{ .flag = "--recovery-did-key=", .field = "recovery_did_key" }, .{ .flag = "--mail-provider=", .field = "mail_provider" }, .{ .flag = "--email-from=", .field = "email_from" }, .{ .flag = "--resend-api-key=", .field = "resend_api_key" }, .{ .flag = "--comail-api-key=", .field = "comail_api_key" }, .{ .flag = "--comail-did=", .field = "comail_did" }, .{ .flag = "--blobstore-path=", .field = "blobstore_path" }, .{ .flag = "--handle-domains=", .field = "handle_domains" }, .{ .flag = "--crawlers=", .field = "crawlers" }, .{ .flag = "--proxy-service-did=", .field = "proxy_service_did" }, .{ .flag = "--proxy-service-id=", .field = "proxy_service_id" }, .{ .flag = "--proxy-service-url=", .field = "proxy_service_url" }, .{ .flag = "--jwt-secret=", .field = "jwt_secret" }, .{ .flag = "--dpop-secret=", .field = "dpop_secret" }, .{ .flag = "--admin-token=", .field = "admin_token" }, .{ .flag = "--log-level=", .field = "log_level" },};
fn env(name: [*:0]const u8) ?[]const u8 { return if (std.c.getenv(name)) |value| std.mem.span(value) else null;}
fn envUsize(name: [*:0]const u8) !?usize { const value = env(name) orelse return null; return try std.fmt.parseInt(usize, value, 10);}
fn envU16(name: [*:0]const u8) !?u16 { const value = env(name) orelse return null; return try std.fmt.parseInt(u16, value, 10);}
fn envBool(name: [*:0]const u8) bool { const value = env(name) orelse return false; return std.ascii.eqlIgnoreCase(value, "1") or std.ascii.eqlIgnoreCase(value, "true") or std.ascii.eqlIgnoreCase(value, "yes") or std.ascii.eqlIgnoreCase(value, "on");}
test "parse split and joined arguments" { var argv = [_][*:0]const u8{ "zds", "--host", "0.0.0.0", "--port=8080", "--db", "dev/test.sqlite3", "--plc-rotation-key=00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", "--recovery-did-key", "did:key:zRecovery", "--mail-provider=comail", "--comail-api-key", "atmos_test", "--comail-did=did:plc:mailer", "--email-from", "ZDS <noreply@example.com>", "--debug", }; const init = std.process.Init.Minimal{ .environ = .empty, .args = .{ .vector = &argv } };
const options = try parse(init);
try std.testing.expectEqualStrings("0.0.0.0", options.host); try std.testing.expectEqual(@as(u16, 8080), options.port); try std.testing.expectEqualStrings("dev/test.sqlite3", options.db_path); try std.testing.expectEqualStrings("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", options.plc_rotation_key.?); try std.testing.expectEqualStrings("did:key:zRecovery", options.recovery_did_key.?); try std.testing.expectEqualStrings("comail", options.mail_provider.?); try std.testing.expectEqualStrings("atmos_test", options.comail_api_key.?); try std.testing.expectEqualStrings("did:plc:mailer", options.comail_did.?); try std.testing.expectEqualStrings("ZDS <noreply@example.com>", options.email_from.?); try std.testing.expectEqualStrings("debug", options.log_level.?);}
test "split arguments require a value" { var argv = [_][*:0]const u8{ "zds", "--db", }; const init = std.process.Init.Minimal{ .environ = .empty, .args = .{ .vector = &argv } };
try std.testing.expectError(error.MissingDatabasePath, parse(init));}