From 4e2525b245782671fab73bfa81561d60a606b841 Mon Sep 17 00:00:00 2001 From: Lewis Date: Sat, 30 May 2026 20:51:25 +0300 Subject: [PATCH] fix(migrate): oauth account:* spec, p256 support Lewis: May this revision serve well! --- frontend/public/oauth-client-metadata.json | 2 +- .../src/lib/migration/offline-flow.svelte.ts | 8 +++- frontend/src/lib/migration/plc-ops.ts | 27 +++++++++++++ frontend/src/tests/migration/plc-ops.test.ts | 39 +++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/frontend/public/oauth-client-metadata.json b/frontend/public/oauth-client-metadata.json index b0693fe..8ca9c0c 100644 --- a/frontend/public/oauth-client-metadata.json +++ b/frontend/public/oauth-client-metadata.json @@ -8,7 +8,7 @@ ], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], - "scope": "atproto transition:generic repo:* blob:*/* rpc:* rpc:com.atproto.server.createAccount?aud=* account:*?action=manage identity:*", + "scope": "atproto transition:generic repo:* blob:*/* rpc:* rpc:com.atproto.server.createAccount?aud=* account:*?action=manage account:repo?action=manage identity:*", "token_endpoint_auth_method": "none", "application_type": "web", "dpop_bound_access_tokens": true diff --git a/frontend/src/lib/migration/offline-flow.svelte.ts b/frontend/src/lib/migration/offline-flow.svelte.ts index 3cb9692..6f1d337 100644 --- a/frontend/src/lib/migration/offline-flow.svelte.ts +++ b/frontend/src/lib/migration/offline-flow.svelte.ts @@ -209,11 +209,15 @@ export function createOfflineInboundMigrationFlow() { } try { - userRotationKeypair = await plcOps.getKeyPair(state.rotationKey.trim()); const { lastOperation } = await plcOps.getLastPlcOpFromPlc(state.userDid); const currentRotationKeys = lastOperation.rotationKeys || []; - if (!currentRotationKeys.includes(userRotationKeypair.didPublicKey)) { + userRotationKeypair = await plcOps.getMatchingKeyPair( + state.rotationKey.trim(), + currentRotationKeys, + ); + + if (!userRotationKeypair) { state.rotationKeyDidKey = ""; return false; } diff --git a/frontend/src/lib/migration/plc-ops.ts b/frontend/src/lib/migration/plc-ops.ts index ed7966f..0e4eaee 100644 --- a/frontend/src/lib/migration/plc-ops.ts +++ b/frontend/src/lib/migration/plc-ops.ts @@ -248,6 +248,33 @@ export class PlcOps { }; } + async getMatchingKeyPair( + privateKeyString: string, + acceptableDidKeys: readonly string[], + ): Promise { + const curves: readonly KeyCurve[] = ["secp256k1", "p256"]; + const results = await Promise.allSettled( + curves.map((curve) => this.getKeyPair(privateKeyString, curve)), + ); + const candidates = results + .filter( + (r): r is PromiseFulfilledResult => + r.status === "fulfilled", + ) + .map((r) => r.value); + if (candidates.length === 0) { + const rejection = results.find( + (r): r is PromiseRejectedResult => r.status === "rejected", + ); + throw rejection?.reason ?? new Error("Unrecognized key format"); + } + return ( + candidates.find((info) => + acceptableDidKeys.includes(info.didPublicKey) + ) ?? null + ); + } + async signAndPublishNewOp( did: string, signingRotationKey: PrivateKey, diff --git a/frontend/src/tests/migration/plc-ops.test.ts b/frontend/src/tests/migration/plc-ops.test.ts index e52fe76..e65edaf 100644 --- a/frontend/src/tests/migration/plc-ops.test.ts +++ b/frontend/src/tests/migration/plc-ops.test.ts @@ -144,6 +144,45 @@ describe("migration/plc-ops", () => { }); }); + describe("getMatchingKeyPair", () => { + it("resolves a P-256 key supplied as hex against its did:key", async () => { + const keypair = await P256PrivateKeyExportable.createKeypair(); + const rawHex = await keypair.exportPrivateKey("rawHex"); + const did = await keypair.exportPublicKey("did"); + + const result = await plcOps.getMatchingKeyPair(rawHex, [did]); + + expect(result?.didPublicKey).toBe(did); + }); + + it("resolves a secp256k1 key supplied as hex against its did:key", async () => { + const keypair = await Secp256k1PrivateKeyExportable.createKeypair(); + const rawHex = await keypair.exportPrivateKey("rawHex"); + const did = await keypair.exportPublicKey("did"); + + const result = await plcOps.getMatchingKeyPair(rawHex, [did]); + + expect(result?.didPublicKey).toBe(did); + }); + + it("returns null when no curve matches the accepted keys", async () => { + const keypair = await P256PrivateKeyExportable.createKeypair(); + const rawHex = await keypair.exportPrivateKey("rawHex"); + + const result = await plcOps.getMatchingKeyPair(rawHex, [ + "did:key:zQ3shqPwo8CSE8zNXEyEpN4ASEBCCNeUFQq8Lrw3zkAJYB7SB", + ]); + + expect(result).toBeNull(); + }); + + it("throws on unparseable input", async () => { + await expect( + plcOps.getMatchingKeyPair("not-a-valid-key", []), + ).rejects.toThrow(); + }); + }); + describe("getKeyPair - JWK format", () => { it("imports secp256k1 JWK with d parameter", async () => { const keypair = await Secp256k1PrivateKeyExportable.createKeypair();