Skip to content

Enforce the DID-matches-document check, relax decode to the canonical schema - #58

Merged
germ-mark merged 1 commit into
mainfrom
llm/verified-shadowed-param-and-decode-laxity
Aug 18, 2026
Merged

Enforce the DID-matches-document check, relax decode to the canonical schema#58
germ-mark merged 1 commit into
mainfrom
llm/verified-shadowed-param-and-decode-laxity

Conversation

@germ-mark

Copy link
Copy Markdown
Contributor

Summary

  • verified(expecting:did:) shadowed its own did: parameter and never checked it — the DID-matches-document-id check Resolver.swift already claims to enforce never actually ran. Fixed on both the sync overload and the async one, which now takes an optional expectedDid: Atproto.DID? = nil (checked only when supplied, so existing callers keep today's behavior).
  • DIDDocument's decode was stricter than the canonical schema (did-doc.ts): context/verificationMethod/service/publicKeyMultibase are now optional, @context accepts a bare string, Service.serviceEndpoint is URL? (an unusable shape decodes to nil rather than failing the whole document). PLC hides this gap; did:web documents are self-hosted and far more likely to hit it.

Sequencing note

Wiring the new expectedDid: parameter into AtprotoClient's and germ-atproto-resolver's call sites is deferred — neither compiles against their currently-pinned AtprotoTypes version, so that edit would be dead code in a repo with its own CI. Follow-up once this releases; the one real call site (germ-atproto-resolver's plcQuery, which today accepts a plc.directory response with no id check at all) is tracked.

Test plan

  • swift build && swift test — 56 tests, 14 suites, all green
  • Reviewed twice before landing (plan + diff) — see commit message for what each pass caught
  • swift format lint -r clean

🤖 Generated with Claude Code

… schema

verified(expecting:did:) shadowed its own did: parameter on the first line
of its body and never checked it, so the DID-matches-document-id check
Resolver.swift's own doc comment already claims to enforce never actually
ran. Fixed on both the sync overload and the async one, which previously had
no way to express an expected DID at all — it now takes an optional
expectedDid: Atproto.DID? = nil, checked only when supplied, so existing
callers passing nothing keep today's behavior. Traced every call site this
session could find; all but one are provable no-ops (the document's id was
always derived from the same DID being compared) — the one real gap is
germ-atproto-resolver's plcQuery, which decodes a plc.directory response
with no id check at all.

DIDDocument's decode was stricter than the canonical schema
(bluesky-social/atproto's did-doc.ts): context, verificationMethod, service,
and publicKeyMultibase are now optional, @context accepts a bare string as
well as an array, and Service.serviceEndpoint is URL? (an object-shaped
endpoint, or anything else that isn't string-or-object, decodes to nil
rather than failing the whole document). PLC-issued documents hid this —
plc.directory emits one uniform, tool-generated shape — but did:web
documents are self-hosted and far more likely to be minimal or
hand-authored. checkServiceForAtproto/pdsUrl throw on a first matching entry
with an unusable endpoint rather than searching past it, matching the DID
spec's own "first matching entry should be used, any others ignored."

Reviewed twice before landing: the plan review found SwiftPM's `from:`
doesn't gate 0.x releases the way SemVer's own convention might suggest (a
0.5.0 lands on any consumer's next resolve, patch or minor makes no
difference — Package.resolved pins are the only real gate), and a third
production call site into the unchecked async overload that hadn't been
found yet (AtprotoClient's verifiedResolve(atIdentifier:) .did leg, reached
by AtprotoOAuth's live authorize-by-DID flow). The diff review found the
service-endpoint doc comment misdescribed why a malformed string becomes
nil (URL(string:) is far more lenient than the comment implied; Service.
validate is the real screen) and that the deliberately-lenient paths on
garbage @context/serviceEndpoint input were undocumented and unpinned. Both
fixed, with tests.

Wiring the new expectedDid: parameter into AtprotoClient's and germ-atproto-
resolver's call sites is deferred — it can't compile against either repo's
currently-pinned AtprotoTypes version, so making that edit now would be
dead, uncommittable code in a repo with its own CI. Follow-up once this
releases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bf05899

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@germ-network/atprototypes Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@germ-mark
germ-mark merged commit 6021ac9 into main Aug 18, 2026
6 checks passed
@germ-mark
germ-mark deleted the llm/verified-shadowed-param-and-decode-laxity branch August 18, 2026 22:24
@github-actions github-actions Bot mentioned this pull request Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant