Repository navigation
Conversation
signPlcOperation signed with SIGNING_KEY regardless of whether it was one of the DID's rotation keys. For accounts migrated with `pds identity` it usually is not, so plc.directory rejected the result and outbound migration failed with an opaque error. It now returns InvalidRequest explaining why. The PLC operation types, audit-log lookup and signing move to src/plc.ts so the CLI can sign operations with a locally held key.
getMigrationToken and signPlcOperation accepted any authenticated caller: app-password sessions, OAuth tokens of any scope, and service JWTs (which getServiceAuth mints for any caller). Together they let any of these get a PLC operation signed by the PDS. That was only harmful where the signing key is a rotation key, which the following commits make the norm. Both endpoints now require the static AUTH_TOKEN, a password session, or an OAuth token granted identity:*. AuthInfo records how the request authenticated so the check can tell app passwords and service JWTs apart from password sessions.
- SourcePdsPlcClient.signPlcOperation takes the fields to change, so callers can set rotationKeys. Adds getRecommendedDidCredentials (to recognise the source PDS's keys) and PlcDirectoryClient.getLatestOperation. - PDSClient gains getRecommendedDidCredentials and signPlcOperation, so the CLI can have the deployed PDS sign for itself. - New rotation-keys utils: ordering (user keys first, PDS key last, max 5), labelling, recovery key generation, and importing pasted private keys. Keys use goat's multibase encoding; checked against goat in both directions for K-256 and P-256. - The signing key backup prompt moves out of init into promptKeyBackup, and the 1Password and file backups take a key kind so they work for recovery keys. init behaves as before. - The CLI's PLC types now come from src/plc.ts.
`pds identity` asked the source PDS to sign an operation that left the rotation keys unchanged, so the source PDS kept control of the DID and Cirrus could not sign PLC operations. It now sets the rotation keys to the keys the user holds, then this PDS's signing key, and removes the source PDS's key. - The source PDS's recommended credentials identify its keys. The user is asked about each other key, and those they hold are kept on top. - If the user holds none, it offers to generate a recovery key, and only adds it after the user confirms it is backed up. - The PDS key comes from the deployed PDS's recommended credentials rather than .dev.vars, and a mismatch with .dev.vars stops the command. - The signed operation is checked for the requested rotation keys before submitting, and read back from plc.directory afterwards. - Key choices happen before requesting the email token, with a summary and a confirmation. - --token now logs in too. Signing needs a session, so that path could not have worked before.
Accounts migrated before `pds identity` set rotation keys still have the previous PDS's key and not Cirrus's. The new command shows the DID's rotation keys and rewrites them as the keys the user holds, then this PDS's key, offering a recovery key if the user holds none. The change is signed by whichever key can: - this PDS, when its key is already a rotation key (adding a recovery key, or dropping keys the user doesn't hold); - the previous PDS, found from the PLC audit log, with its password and an email code; - a rotation key the user pastes in (goat multibase or hex), signed locally. The signed operation is checked against the requested keys and endpoint before submitting, and read back afterwards. Key selection now defaults to keeping keys whose holder is unknown, since pressing enter could otherwise drop a user's recovery key.
For did:plc accounts whose DID points at this PDS, status now reports whether the PDS's key is a rotation key (an error if not, since the PDS then can't update the identity) and warns when it is the only one, pointing to `pds rotation-keys` in both cases.
- README: `pds identity` steps, the new `pds rotation-keys` command, a note that outbound migration needs the PDS to be a rotation key, and a Rotation and Recovery Keys section covering priority, the 72-hour override and using the recovery key with goat. The goat commands were run against a local PLC directory with a generated recovery key. - Root README Key Safety: the signing key is now also a rotation key, and migration offers a recovery key. - Plans: record what was built and tested, and what still needs a live test; note rotation keys in the migration plan. - Changeset.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
pdscheck | efae325 | Oct 05 2026, 06:06 AM |
commit: |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
cirrusdocs | efae325 | Commit Preview URL Branch Preview URL |
Oct 05 2026, 06:06 AM |
|
#250 ( It also refuses service JWTs in Tested together on a did:plc instance where the Cirrus key is already a rotation key: handle changes there write PLC operations through |
Summary
Migrating a did:plc account left the previous PDS holding the only rotation key.
pds identityasked the source PDS to sign an operation withrotationKeys: undefined, so the Cirrus signing key became a verification method but never a rotation key. As a result Cirrus could not sign any change to the DID:signPlcOperationsigned with a key that wasn't allowed to.This PR makes the migrated account's rotation keys
[keys the user holds…, Cirrus signing key], removes the old PDS's key, and offers a recovery key that ranks above Cirrus. It adds a command to fix already-migrated accounts, and locks down who can get the PDS to sign PLC operations.Commits
docs(plans): the plan (plans/in-progress/plc-rotation-keys.md, updated with results in the last commit).fix(pds): refuse to sign PLC operations without a rotation key.signPlcOperationreturns a clear error instead of an operation plc.directory will reject. PLC types, audit-log lookup and signing move tosrc/plc.ts.fix(pds): require full access to sign PLC operations.getMigrationTokenandsignPlcOperationaccepted app passwords, OAuth tokens of any scope, and service JWTs. Once Cirrus is a rotation key that would let any signed-in app take over the identity, so this lands before the commits that make it one. They now needAUTH_TOKEN, a password session, or OAuth withidentity:*.feat(pds): CLI helpers. PLC client changes, rotation key ordering and labelling, recovery key generation, and import of pasted keys. Keys use goat's multibase encoding. The signing key backup prompt moves out ofinitso recovery keys can reuse it.feat(pds):pds identitysets rotation keys. It identifies the source PDS's keys via itsgetRecommendedDidCredentials, asks about any others, and offers a recovery key (added only after the user confirms it's backed up). It takes the PDS key from the deployed PDS rather than.dev.vars, checks the signed operation before submitting, and reads it back afterwards.--tokennow logs in too; it couldn't have worked before.feat(pds):pds rotation-keys. Fixes already-migrated accounts. The change is signed by the PDS (if already a rotation key), the previous PDS (found from the PLC audit log; password plus email code), or a rotation key the user pastes (signed locally).feat(pds):pds statusreports whether the PDS can update the identity and whether there's a recovery key.docs(pds): README (commands, a Rotation and Recovery Keys section with goat emergency steps), root README Key Safety, and changesets.Notes for review
getServiceAuthmints service JWTs with anyaud/lxmfor any authenticated caller, and a self-addressed one passesrequireAuthwith full trust. The PLC endpoints now refuse service JWTs, but the wider issue remains.getLatestPlcOperation/signOperationfromsrc/plc.ts. With this merged, its PLC path works for migrated accounts.Test plan
expect, with fetch routed to a fake PLC directory and fake PDSes using real genesis ops, DIDs and CIDs. After each scenario the whole op log was verified with goat'sVerifyOpLog(the reference Go implementation):pds identityfrom a PDS holding the only key, creating a recovery key →[recovery, pds]pds rotation-keyswith nothing to change → nothing submitted[recovery, pds][user key, pds]pds statusfor each statepds identity: check the audit log has[recovery, cirrus]and Bluesky's key is gone. Confirm bsky.social's key is recognised (not offered as "probably a recovery key you added"); that depends on itsgetRecommendedDidCredentialsreturning the key that is in the PLC logpds rotation-keyson an account migrated with the previous CLI, signing through bsky.social (does a deactivated account still get an email code?)