perlsky#
perlsky is a Perl 5 implementation of an AT Protocol Personal Data Server.
Current direction:
- Official
com.atproto.*lexicons are vendored intoshare/lexicons. - The external XRPC surface is loaded from those lexicons at runtime.
- Account, repo, blob, sync, CAR, DAG-CBOR, CID, and MST support are being implemented in native Perl.
- The app is designed to run self-contained with SQLite and filesystem blob storage.
The immediate goal is a PDS that is pleasant to hack on and interoperable enough to be exercised with real AT Protocol clients and repo sync tooling.
Developer-oriented notes about the current facade/module split live in docs/CODE_STRUCTURE.md.
Reference differential validation:
- Run
script/differential-validateto compareperlskyagainst the official published@atproto/pdson a focused set of account, repo, moderation, sync, firehose, andimportReposnapshot-restore behaviors. - The differential harness also configures a local relay/crawler mock for both servers and verifies that both emit
com.atproto.sync.requestCrawlnotices with the expected hostname after repo activity, based on the upstream crawler wiring inpackages/pds/src/crawlers.ts,context.ts, andsequencer.ts. - Run
PERLSKY_DIFF_ACCOUNT_DID_METHOD=did:plc script/differential-validateto exercise the same harness in PLC-account mode, including recommended DID credentials, PLC signature requests, PLC handle updates, token-gated PLC signing behavior, and moderation checks after PLC handle changes. - The helper installs the reference runtime into
.tools/reference-runtimewith Node 20 viafnm. - Run
PERLSKY_RUN_REFERENCE_DIFF=1 prove -lv t/reference-differential.tto exercise the same harness from the test suite. - Run
PERLSKY_RUN_REFERENCE_DIFF=1 prove -lv t/reference-differential-plc.tto run the PLC-specific reference comparison from the test suite.
Metrics and observability:
perlskynow exposes Prometheus-compatible metrics at/metrics.- Set
metrics_tokento requireAuthorization: Bearer <token>for scrapes. - The main runtime signals cover XRPC request counts/latency, websocket subscriptions and emitted frames, crawler notifications, blob ingress/egress bytes, and key store operation timings.
- Detailed operator documentation lives in
docs/METRICS.md.
Deployment and first-account setup:
- Generic single-node deployment instructions live in
docs/DEPLOYMENT.md. - The deployment guide includes a reverse-proxy layout, a sample
systemdunit, validation commands, and acreateAccountexample for bootstrapping the first user. perlskynow includes a built-in ATProto OAuth provider surface, so modern third-party clients that use the Bluesky OAuth flow can authenticate directly against your PDS without extra auth-server infrastructure.- The built-in provider publishes
/.well-known/oauth-protected-resource,/.well-known/oauth-authorization-server,/oauth/jwks,/oauth/par,/oauth/authorize,/oauth/token, and/oauth/revokefrom the same host as the PDS. - OAuth scope enforcement now understands both the transition scopes (
transition:generic,transition:email,transition:chat.bsky), the newer granular permission families (account:,identity:,repo:,blob:, andrpc:), andinclude:<nsid>permission-set scopes, so clients that request narrower ATProto permissions can be authorized without silently getting broader access. - If
service_handle_domainisexample.com, submittinghandle: "alice"tocom.atproto.server.createAccountcreatesalice.example.com. - If
invite_code_requiredis enabled, public signup is disabled until a valid invite code is supplied. com.atproto.server.createInviteCodeandcom.atproto.server.createInviteCodesare admin-only by default. Setself_service_invite_codesto enable self-service invite minting for authenticated full-access sessions, limited to the caller's own account.script/perlsky-admin create-invitecan mint invite codes locally on the server without needing an existing user session.- The invite-only bootstrap flow is documented with copy-pasteable commands in
docs/DEPLOYMENT.md. - Browser clients such as
bsky.appcan talk toperlskydirectly because XRPC and DID-document responses include CORS headers and answer OPTIONS preflight requests. - OAuth clients such as Tangled can also discover and use
perlskydirectly as both the protected resource and authorization server, using PAR, PKCE,private_key_jwt, and DPoP as required by the ATProto OAuth profile. - Unknown
app.bsky.*requests are proxied tohttps://api.bsky.appby default, and unknownchat.bsky.*requests are proxied tohttps://api.bsky.chatby default using per-account service-auth JWTs. - Set
bsky_appview_url/bsky_appview_didorchat_service_url/chat_service_didin your config if you want different upstream services.
Relay / crawler discovery:
- Configure
hostnameto the public host name you want relays to crawl, for examplepds.example.com. This should be the host, not the full URL. - Configure
crawlersas a list of relay or crawler service origins, for example["https://bsky.network"]. perlskywill POSTcom.atproto.sync.requestCrawlto each configured crawler after local repo/account/identity activity, while throttling repeat notices withcrawler_notify_interval(default1200seconds).- Local regression coverage for this path lives in
t/crawlers.t.
Browser smoke:
script/perlsky-browser-smoke run-dualnow prefers a saved reusable account pair from.cache/browser-smoke/reusable-pair.json, so the default dual-account smoke does not create new actors.- Use
script/perlsky-browser-smoke bootstrap-paironce to mint and save a dedicated smoke pair, then rerunscript/perlsky-browser-smoke run-dualas often as you want against those same accounts. - Use
script/perlsky-browser-smoke show-pairto inspect the saved pair metadata andscript/perlsky-browser-smoke clear-pairto forget it locally. - The reusable dual-account smoke now covers list lifecycle plus deeper
bsky.appsettings flows, and it pre-cleans old smoke-created posts/lists before each run. - DMs are intentionally deferred from the current browser-smoke tranche; see
docs/BROWSER_SMOKE.mdfor current scope and rationale. - Fresh-account creation is still available through the explicit
bootstrap-*commands, but it is no longer the normal path for repeated browser smoke runs. - Detailed browser-smoke workflow, current interaction coverage, and the env-gated
provewrapper live indocs/BROWSER_SMOKE.md. - Extraction work toward a cross-PDS standalone package now lives in the
atproto-smokerepo, which owns the browser runtime, package CLI, example configs, and bring-your-own-account plusperlskyadapter helpers. script/perlsky-browser-smokeexpects a standalone checkout at../atproto-smokeby default. SetPERLSKY_BROWSER_SUITE_ROOTto point it at any other checkout explicitly.- The lowest-friction local layout is to clone
atproto-smokenext toperlsky, so both repos share the same parent directory. - The shared smoke runtime now applies a bounded per-step timeout (
stepTimeoutMs, default120000) so late browser stalls fail with artifacts instead of hanging forever.
Moderation and labels:
com.atproto.admin.updateSubjectStatusnow enforces repo, record, and blob takedowns as real behavior instead of passive metadata.- Repo takedowns block ordinary login, repo writes, and public repo reads.
allowTakendownsessions are accepted for parity with the reference PDS, but those sessions still cannot write. - Record takedowns hide records from
com.atproto.repo.getRecordandcom.atproto.repo.listRecords. - Blob takedowns quarantine blob reads for the public while still permitting authenticated self/admin recovery access, and they block both duplicate blob uploads and new record writes that reference quarantined blobs.
com.atproto.label.queryLabels,com.atproto.label.subscribeLabels, andcom.atproto.temp.fetchLabelsare backed by persisted local labels rather than synthesized snapshots. Admin takedowns emit!hidelabels and restores emit negation events.- Label query/stream behavior is covered by local regression tests in
t/labels.t. The official reference PDS does not provide a like-for-like local labeler implementation to diff against, so direct upstream differential checks are focused on moderation semantics rather than label RPC parity.
Interop fixtures:
t/crypto-interop.tloads the official Blueskytools/reference/atproto/interop-test-files/crypto/w3c_didkey_K256.jsonvectors so secp256k1did:keyencoding stays pinned to the same public fixtures as the upstream stack.t/plc-identity.tdrivesperlskyagainst the local PLC mock built on the official@did-plc/lib, covering account creation, recommended DID credentials, PLC handle updates, token-gated PLC signing, and validated PLC submission semantics.