Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/minidoc-carries-signing-key.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 5 additions & 5 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -51,7 +52,11 @@ let package = Package(
),
.testTarget(
name: "MicrocosmTests",
dependencies: ["Microcosm", "MicrocosmMocks"]
dependencies: [
"Microcosm",
"MicrocosmMocks",
.product(name: "AtprotoTypesVerify", package: "AtprotoTypes"),
]
),
]
)
16 changes: 15 additions & 1 deletion Sources/Microcosm/Slingshot/Lexicon/Request/ResolveMiniDoc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
54 changes: 54 additions & 0 deletions Tests/MicrocosmTests/MiniDocDocumentTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// 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)
}

///`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")
func stillResolvesPDS() throws {
let miniDoc = try Lexicon.Blue.Microcosm.Identity.ResolveMiniDoc.Output.mock()
#expect(try miniDoc.didDocument.pdsUrl == miniDoc.pds)
}
}