···7373- `ZDS_INVITE_REQUIRED`: set to `true` to require invite codes for account
7474 creation. `describeServer` reflects this value.
75757676+PLC key lifecycle:
7777+7878+- Repo signing keys are for repository commits only.
7979+- PLC rotation keys are for DID PLC operations only.
8080+- `getRecommendedDidCredentials` follows the official PDS shape: the account
8181+ signing key is returned under `verificationMethods.atproto`; rotation keys
8282+ are the optional recovery DID key followed by the configured PDS rotation key.
8383+- ZDS rejects new PLC operations that put the account signing key in
8484+ `rotationKeys`.
8585+- Existing early accounts can be repaired with `just plc-repair <did>` after
8686+ setting `ZDS_DB` or `ZDS_DB_PATH`, `ZDS_PUBLIC_URL`, `ZDS_PLC_ROTATION_KEY`,
8787+ and optional `ZDS_RECOVERY_DID_KEY`. The repair tool is intentionally separate
8888+ from normal XRPC handling; it only uses the account signing key if the current
8989+ public PLC state already authorizes that key as a rotation key, and submits a
9090+ replacement operation containing the intended recovery/PDS rotation keys.
9191+7692When invites are required and the database has no accounts or invite codes, ZDS
7793creates one bootstrap code and logs it during startup. Invite codes are stored
7894as metadata rows and uses are recorded separately, matching the official PDS
+4
justfile
···2424 --data "$body" |
2525 jq -r '.code'
26262727+# repair an existing account's PLC rotation set from the local database
2828+plc-repair did:
2929+ zig build plc-repair -- {{did}}
3030+2731# build the container image locally
2832docker-build tag="latest" platform="linux/amd64" arch="amd64":
2933 DOCKER_BUILDKIT=1 docker build --platform {{platform}} --build-arg TARGETARCH={{arch}} --build-arg ZDS_VERSION={{tag}} -t zds:{{tag}} .
+5-7
src/atproto/identity.zig
···186186fn plcOperationSigningKeypair(
187187 request: *http.Server.Request,
188188 allocator: std.mem.Allocator,
189189- account: auth.Account,
189189+ _: auth.Account,
190190 last_op: std.json.Value,
191191) !zat.Keypair {
192192- var account_keypair = try store.signingKeypair(account.did);
193193- const account_did_key = try account_keypair.did(allocator);
194194- if (jsonArrayContainsString(zat.json.getPath(last_op, "rotationKeys"), account_did_key)) {
195195- return account_keypair;
196196- }
197192 var rotation_keypair = plc.configuredRotationKeypair() catch |err| switch (err) {
198193 error.MissingPlcRotationKey => {
199194 try http_api.xrpcError(request, .internal_server_error, "InternalServerError", "PLC rotation key is not configured");
···208203 if (jsonArrayContainsString(zat.json.getPath(last_op, "rotationKeys"), rotation_did_key)) {
209204 return rotation_keypair;
210205 }
211211- try http_api.xrpcError(request, .bad_request, "InvalidRequest", "No controlled key matches current PLC rotation keys");
206206+ try http_api.xrpcError(request, .bad_request, "InvalidRequest", "PDS rotation key is not authorized by current PLC rotation keys");
212207 return error.HandledResponse;
213208}
214209···233228 };
234229 if (!jsonArrayContainsString(object.get("rotationKeys"), rotation_did_key)) {
235230 return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Rotation keys do not include server rotation key");
231231+ }
232232+ if (jsonArrayContainsString(object.get("rotationKeys"), did_key)) {
233233+ return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Rotation keys must not include the account signing key");
236234 }
237235 const verification_methods = object.get("verificationMethods") orelse {
238236 return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing verificationMethods");
···330330 if (!jsonArrayContainsString(object.get("rotationKeys"), server_rotation_key)) {
331331 return http_api.xrpcError(request, .bad_request, "InvalidRequest", "PLC operation does not include server rotation key");
332332 }
333333+ if (jsonArrayContainsString(object.get("rotationKeys"), signing_did_key)) {
334334+ return http_api.xrpcError(request, .bad_request, "InvalidRequest", "PLC operation must not include reserved signing key as a rotation key");
335335+ }
333336 const also_known_as = try std.fmt.allocPrint(allocator, "at://{s}", .{handle});
334337 if (!jsonArrayFirstStringEquals(object.get("alsoKnownAs"), also_known_as)) {
335338 return http_api.xrpcError(request, .bad_request, "InvalidRequest", "PLC operation handle does not match account handle");