fix(extension): resolve agent names from a stage 1 a browser can read - #120
Merged
Conversation
Typing an agent name into the agent-address field always failed with a bare
"Failed to fetch". Stage 1 read the DID out of a `Location` header:
const res = await fetch(url, { redirect: "manual" });
return { status: res.status, location: res.headers.get("location") };
A browser can never do that, for two independent reasons:
- `redirect: "manual"` does not hand back the redirect. It returns an
opaque-redirect response — status 0, no headers — so `get("location")`
is null for every redirect, on every host.
- The contract's own answer is a `did:` URI, and Chrome rejects a redirect
to a non-web-safe scheme in the network stack (`net::ERR_UNSAFE_REDIRECT`)
before any redirect mode is consulted. The fetch rejects outright with
`TypeError: Failed to fetch`, which is the string users saw.
No extension API rescues this: `webRequest` never fires the callback that
would carry the header, because the redirect notification is suppressed below
it. The code only passed its tests because Node's `fetch` does expose
`Location` — so the tests now cover both transports.
The webvh hosting service already content-negotiates (`resolve_agent_name.rs`):
a browser-shaped `Accept` gets an ordinary same-origin 302 to the DID's log,
which fetch follows normally. So stage 1 now asks for an answer it can read.
- `fetchAgentName` (background.ts) sends `Accept: application/json,
application/did+json;q=0.9, text/html;q=0.8`, follows the redirect, and
stream-reads the body under a 256 KiB cap (R1.2's foreign-fetch profile —
`text()` then `slice()` is the same unbounded read wearing a cap).
- A refused request becomes a coded `agent-name/unreadable` error that names
the way out — paste the full `did:…` — instead of leaking "Failed to
fetch". Distinct from `no-redirect`, which means the server *was* read and
named no DID (R3.7: callers branch on the code, never the message).
- `didFromNameResponse` (agent-name.ts) implements the guide's deliberately
permissive contract in one place: a bare `did:` in `Location`, `?did=`,
the final path segment percent-decoded, or a body carrying `{did}`, a
document `id`, `didDocument.id`, or a did:webvh log's `state.id`. It
ignores the request URL itself, so a DID is never derived from the name's
own spelling.
Stages 2 and 3 are untouched and still mandatory. That is what makes the
permissive extraction safe: whatever stage 1 finds is a *candidate* DID that
must still resolve and claim the name back via `alsoKnownAs`, so a wrong guess
fails closed rather than granting anything.
Verified end to end against the live server: `dids.firstperson.dev/@geoffturk`
lands on `/motion-knife/did.jsonl`, yields
`did:webvh:Qmaye5…:dids.firstperson.dev:motion-knife`, whose latest log entry
carries `alsoKnownAs: ["https://dids.firstperson.dev/@geoffturk"]`.
Also moves two doc comments that had drifted onto `handleApproverState` back
onto the functions they describe.
Known gap, unchanged by this: a name server that only ever answers with a bare
`did:` redirect stays unresolvable from any browser. The design guide marks the
redirect contract "unpinned"; it should require a browser-readable form.
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78
force-pushed
the
fix/agent-name-browser-stage-1
branch
from
August 18, 2026 12:23
4c2816c to
fe6bae8
Compare
stormer78
added a commit
that referenced
this pull request
Aug 19, 2026
`fix(extension): resolve agent names from a stage 1 a browser can read (#120)` landed on main and touched the same import block this branch rewrites. The conflict is the two changes meeting head-on: main added `AGENT_NAME_UNREADABLE` to the `agent-name.js` import and still imports `cookie-scope.js` beside it, while this branch deletes `cookie-scope.ts` outright along with the password-site login that needed it. Resolution keeps main's `AGENT_NAME_UNREADABLE` — it is used at two call sites in `background.ts` — and drops the `cookie-scope.js` import, whose module no longer exists. Nothing else in the tree references `checkInjectableOrigin` or `cookieDomainScope`. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Typing an agent name (
dids.firstperson.dev/@geoffturk) into the agent-addressfield always failed with a bare "Failed to fetch". Pasting the full
did:webvh:…worked. Agent names have never resolved from the extension.Root cause
Stage 1 read the DID out of a
Locationheader:A browser can never do that, for two independent reasons — both confirmed
live in Chrome against the real server:
redirect: "manual"does not hand back the redirect. It returns anopaque-redirect response — status 0, no headers at all — so
headers.get("location")is null for every redirect, on every host.did:URI, and Chrome rejects a redirect toa non-web-safe scheme down in the network stack (
net::ERR_UNSAFE_REDIRECT)before any redirect mode is consulted. The fetch rejects outright with
TypeError: Failed to fetch— the string in the bug report.Measured, from a page on an unrelated origin:
fetch(name, {redirect: "manual"})TypeError: Failed to fetchfetch(name, {redirect: "follow"})TypeError: Failed to fetchfetch(name, {redirect: "error"})TypeError: Failed to fetchfetch("https://dids.firstperson.dev/")200,type: "cors"So it is neither CORS nor the host permission — the host is reachable and the
302 even carries
access-control-allow-origin: *. It is thedid:target.No extension API rescues this either:
webRequestnever fires the callbackthat would carry the header, because the redirect notification is suppressed
below it. The code passed its tests only because Node's
fetchdoes exposeLocation, so the unit tests now cover both transports.The fix
affinidi-webvh-servicealready content-negotiates(
did-hosting-server/src/routes/resolve_agent_name.rs): a browser-shapedAcceptgets an ordinary same-origin302 → /{mnemonic}/did.jsonl, whichfetch follows normally. Stage 1 now asks for an answer it can read.
fetchAgentName(background.ts) sendsAccept: application/json, application/did+json;q=0.9, text/html;q=0.8,follows the redirect, and stream-reads the body under a 256 KiB cap
(R1.2 foreign-fetch profile —
text()thenslice()is the same unboundedread wearing a cap, and
content-lengthis no substitute since a server thatomits it is the one worth defending against).
agent-name/unreadableerror namingthe way out ("paste the full
did:…") instead of leakingFailed to fetch.Deliberately distinct from
no-redirect, which means the server was readand named no DID (R3.7 — callers branch on the code, never the message).
didFromNameResponse(agent-name.ts) implements the guide'sdeliberately permissive redirect contract in one place: a bare
did:inLocation,?did=, the final path segment percent-decoded, or a bodycarrying
{did}, a documentid,didDocument.id, or a did:webvh log'sstate.id. It ignores the request URL itself, so a DID is never derived fromthe name's own spelling.
Stages 2 and 3 are untouched and still mandatory, which is what makes the
permissive extraction safe: whatever stage 1 finds is a candidate DID that
must still resolve and claim the name back via
alsoKnownAs. A wrong guessfails closed rather than granting anything — covered by a test that feeds stage
1 somebody else's DID and asserts
not-authorized.Also moves two doc comments that had drifted onto
handleApproverStatebackonto the functions they describe.
Verification
End to end against the live server:
Five new unit tests: the browser path (followed redirect + JSONL log), every
DID-carrying shape, a landing page that names nothing, a candidate that fails
stage 3, and an unreadable transport.
94/94extension,215/215core,51/51tsp-js; lint + build clean; MV3 single-bundle invariant holds.Known gap (not introduced here)
A name server that only ever answers with a bare
did:redirect staysunresolvable from any browser — no client-side change can fix that. The design
guide marks the redirect contract "unpinned"; it should require a
browser-readable form. Worth a follow-up in
agent-names-design-guide.md, sinceaffinidi-webvh-servicealready does the right thing but nothing requires it.Pre-merge checklist