From e79a586a1f1085cd1aa0e80c2e6828e414da0124 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Wed, 19 Aug 2026 00:23:03 -0700 Subject: [PATCH 1/4] Publish the atproto signing key in resolveMiniDoc's document MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slingshot returns signing_key on the wire but the adapter discarded it, shipping verificationMethod: [] — so a repo proof checked against a Slingshot-resolved document failed for want of a key, nondeterministically, since optimizedResolve races this resolver against plc.directory and either can win. Spelled the way plc.directory spells it: fully-qualified did:...#atproto id, Multikey, self-controlled. Requires AtprotoTypes 0.5.1, where VerificationMethod's initializer became public (germ-network/AtprotoTypes#60). CI on this PR won't go green until that releases and Package.resolved is updated to match. Co-Authored-By: Claude Sonnet 5 --- .changeset/minidoc-carries-signing-key.md | 7 ++++ Package.swift | 9 ++++- .../Lexicon/Request/ResolveMiniDoc.swift | 16 +++++++- .../MicrocosmTests/MiniDocDocumentTests.swift | 37 +++++++++++++++++++ 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 .changeset/minidoc-carries-signing-key.md create mode 100644 Tests/MicrocosmTests/MiniDocDocumentTests.swift diff --git a/.changeset/minidoc-carries-signing-key.md b/.changeset/minidoc-carries-signing-key.md new file mode 100644 index 0000000..95c8253 --- /dev/null +++ b/.changeset/minidoc-carries-signing-key.md @@ -0,0 +1,7 @@ +--- +"@germ-network/microcosm": minor +--- + +`resolveMiniDoc`'s `Atproto.DIDDocument` adapter now publishes the `#atproto` verification method instead of an empty list. Slingshot returns `signing_key` on the wire and the adapter discarded it, so a repo proof checked against a Slingshot-resolved document failed for want of a key — nondeterministically, since `optimizedResolve` races this resolver against plc.directory and either can win. The method is spelled the way plc.directory spells it — fully-qualified `did:...#atproto` id, `Multikey`, self-controlled — so the two resolvers now agree field for field. + +Requires AtprotoTypes 0.5.1, where `VerificationMethod`'s initializer became public. diff --git a/Package.swift b/Package.swift index 01a10c6..cedc271 100644 --- a/Package.swift +++ b/Package.swift @@ -16,8 +16,9 @@ let package = Package( ], dependencies: [ .package( + //0.5.1 is the release that makes VerificationMethod.init public url: "https://github.com/germ-network/AtprotoTypes.git", - from: "0.4.5" + from: "0.5.1" ), .package( url: "https://github.com/germ-network/GermConvenience.git", @@ -51,7 +52,11 @@ let package = Package( ), .testTarget( name: "MicrocosmTests", - dependencies: ["Microcosm", "MicrocosmMocks"] + dependencies: [ + "Microcosm", + "MicrocosmMocks", + .product(name: "AtprotoTypesVerify", package: "AtprotoTypes"), + ] ), ] ) diff --git a/Sources/Microcosm/Slingshot/Lexicon/Request/ResolveMiniDoc.swift b/Sources/Microcosm/Slingshot/Lexicon/Request/ResolveMiniDoc.swift index 9f2fba7..995641b 100644 --- a/Sources/Microcosm/Slingshot/Lexicon/Request/ResolveMiniDoc.swift +++ b/Sources/Microcosm/Slingshot/Lexicon/Request/ResolveMiniDoc.swift @@ -71,12 +71,26 @@ extension Lexicon.Blue.Microcosm.Identity.ResolveMiniDoc: Atproto.XRPC.ResponseP } extension Lexicon.Blue.Microcosm.Identity.ResolveMiniDoc.Output { + ///Slingshot returns the repo signing key on the wire, so the document it + ///backs can carry it rather than making a consumer re-resolve through + ///plc.directory for the one field a repo proof needs. public var didDocument: Atproto.DIDDocument { .init( context: [], id: did.rawValue, alsoKnownAs: ["at://" + handle.rawValue], - verificationMethod: [], + //spelled the way plc.directory spells it — fully-qualified id, + //`Multikey`, self-controlled — because `optimizedResolve` races + //this resolver against that one, and a consumer holding the result + //should not be able to tell which won. + verificationMethod: [ + .init( + id: did.rawValue + "#atproto", + type: "Multikey", + controller: did.rawValue, + publicKeyMultibase: signingKey + ) + ], service: [ .init( id: "#atproto_pds", diff --git a/Tests/MicrocosmTests/MiniDocDocumentTests.swift b/Tests/MicrocosmTests/MiniDocDocumentTests.swift new file mode 100644 index 0000000..b4fa3c4 --- /dev/null +++ b/Tests/MicrocosmTests/MiniDocDocumentTests.swift @@ -0,0 +1,37 @@ +// +// MiniDocDocumentTests.swift +// Microcosm +// +// Created by Mark @ Germ on 8/19/26. +// + +import AtprotoTypes +import AtprotoTypesVerify +import Foundation +import Microcosm +import MicrocosmMocks +import Testing + +///The contract worth pinning is not that the field is populated but that the +///repo verifier accepts what it holds — `RepoSigningKey` matches on fragment, +///controller and multibase, and getting any one of the three wrong leaves the +///document as unusable as the empty list it replaced. +struct MiniDocDocumentTests { + @Test("the miniDoc document publishes a signing key the repo verifier accepts") + func carriesAtprotoSigningKey() throws { + let miniDoc = try Lexicon.Blue.Microcosm.Identity.ResolveMiniDoc.Output.mock() + let document = miniDoc.didDocument + + let key = try RepoSigningKey(atprotoKeyIn: document, did: miniDoc.did) + #expect(key.curve == .secp256k1) + #expect(key.compressedPoint.count == 33) + } + + ///Guards the rest of the literal: the PDS entry sits in the same + ///initializer the signing key was added to. + @Test("populating the signing key leaves the PDS entry reachable") + func stillResolvesPDS() throws { + let miniDoc = try Lexicon.Blue.Microcosm.Identity.ResolveMiniDoc.Output.mock() + #expect(try miniDoc.didDocument.pdsUrl == miniDoc.pds) + } +} From 06ae3508a0fdca2d2d977d0f760753e69f722fd0 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Wed, 19 Aug 2026 00:44:52 -0700 Subject: [PATCH 2/4] Pin the plc.directory-parity shape, not just RepoSigningKey's matcher RepoSigningKey accepts a bare "#atproto" fragment and doesn't care about the type string, so the fragment/controller/multibase test alone let both regress silently: a bare fragment or a wrong verificationMethod type passed every existing check while breaking the parity with plc.directory this adapter exists to match. Co-Authored-By: Claude Sonnet 5 --- Tests/MicrocosmTests/MiniDocDocumentTests.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Tests/MicrocosmTests/MiniDocDocumentTests.swift b/Tests/MicrocosmTests/MiniDocDocumentTests.swift index b4fa3c4..baa787e 100644 --- a/Tests/MicrocosmTests/MiniDocDocumentTests.swift +++ b/Tests/MicrocosmTests/MiniDocDocumentTests.swift @@ -27,6 +27,21 @@ struct MiniDocDocumentTests { #expect(key.compressedPoint.count == 33) } + ///`RepoSigningKey`'s matcher accepts a bare `#atproto` fragment too, so the + ///test above can't tell the fully-qualified form from that one — but + ///plc.directory always emits the fully-qualified form, and a consumer + ///racing this resolver against that one shouldn't see two documents that + ///differ in shape depending on which won. + @Test("the published method matches plc.directory's shape, not just RepoSigningKey's matcher") + func matchesPlcDirectoryShape() throws { + let miniDoc = try Lexicon.Blue.Microcosm.Identity.ResolveMiniDoc.Output.mock() + let method = try #require(miniDoc.didDocument.verificationMethod?.first) + + #expect(method.id == miniDoc.did.rawValue + "#atproto") + #expect(method.type == "Multikey") + #expect(method.controller == miniDoc.did.rawValue) + } + ///Guards the rest of the literal: the PDS entry sits in the same ///initializer the signing key was added to. @Test("populating the signing key leaves the PDS entry reachable") From fc196f2ee77ff7e60f4f05f95e1bcb3312326098 Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Wed, 19 Aug 2026 10:51:55 -0700 Subject: [PATCH 3/4] Resolve against the released AtprotoTypes 0.5.1 VerificationMethod.init is public as of this release. AtprotoClient moves along transitively (0.5.7 -> 0.5.10, its own floor's normal drift). Co-Authored-By: Claude Sonnet 5 --- Package.resolved | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Package.resolved b/Package.resolved index 39a0273..5868ad2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "99d3ab02a15f12d5e27c26e770f041b5493829070090625708db51eba843b29f", + "originHash" : "9c43b61f87917b87bdf423b78feded31e7d5743933721bdf81541f241a901b04", "pins" : [ { "identity" : "atprotoclient", "kind" : "remoteSourceControl", "location" : "https://github.com/germ-network/AtprotoClient.git", "state" : { - "revision" : "c92abfd74defeb60c09b45b8ab11e215987920a3", - "version" : "0.5.7" + "revision" : "f4c74fdaf033a720532352919b3d80ce8cef23a4", + "version" : "0.5.10" } }, { @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/germ-network/AtprotoTypes.git", "state" : { - "revision" : "12207f3ede15d1a4e6997a0bac6b3718400d2a5f", - "version" : "0.4.5" + "revision" : "9d0b6bba891c1a7dcfac21768f7c4c77c6aa06e7", + "version" : "0.5.1" } }, { From 8d53136457d847155d6d6f0ae8444647da67f7ea Mon Sep 17 00:00:00 2001 From: Mark Xue Date: Wed, 19 Aug 2026 10:59:16 -0700 Subject: [PATCH 4/4] Wrap the long @Test title to satisfy swift-format --strict Co-Authored-By: Claude Sonnet 5 --- Tests/MicrocosmTests/MiniDocDocumentTests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/MicrocosmTests/MiniDocDocumentTests.swift b/Tests/MicrocosmTests/MiniDocDocumentTests.swift index baa787e..3a9cf87 100644 --- a/Tests/MicrocosmTests/MiniDocDocumentTests.swift +++ b/Tests/MicrocosmTests/MiniDocDocumentTests.swift @@ -32,7 +32,9 @@ struct MiniDocDocumentTests { ///plc.directory always emits the fully-qualified form, and a consumer ///racing this resolver against that one shouldn't see two documents that ///differ in shape depending on which won. - @Test("the published method matches plc.directory's shape, not just RepoSigningKey's matcher") + @Test( + "the published method matches plc.directory's shape, not just RepoSigningKey's matcher" + ) func matchesPlcDirectoryShape() throws { let miniDoc = try Lexicon.Blue.Microcosm.Identity.ResolveMiniDoc.Output.mock() let method = try #require(miniDoc.didDocument.verificationMethod?.first)