Skip to content

feat(demo-rp): a test website for the wallet's RP login paths - #140

Merged
stormer78 merged 1 commit into
mainfrom
feat/rp-login-test-harness
Aug 29, 2026
Merged

feat(demo-rp): a test website for the wallet's RP login paths#140
stormer78 merged 1 commit into
mainfrom
feat/rp-login-test-harness

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

Yes, we can test this ourselves — this is the harness

Both RP-login flows — REST SIOPv2 (login) and DIDComm (loginDidcomm) — could only be exercised against a real deployment, so in practice they were not exercised at all.

One of them was broken for an unknown length of time as a direct result: the wallet sent a retired authenticate Type URI the control plane no longer routes, so a site-initiated login failed after the user had already approved the consent prompt, and nothing surfaced it (#139).

This serves a page that calls window.vtaWallet the way a relying party does, against a control plane of your choosing — the live one, or a did-hosting-control on localhost — and shows what came back.

CONTROL_DID=did:webvh:… MEDIATOR_DID=did:webvh:… \
  npm run login-harness --workspace @openvtc/pnm-demo-rp
# http://127.0.0.1:4041

What it deliberately does not do

It asserts nothing. The point is to make the round-trip observable next to the wallet's own console, which is where the diagnosis actually happens. A harness that judged the result would be a worse unit test with a network dependency attached — the real assertions belong in rp-login.didcomm.mjs, where they now are.

It is kept out of server.mjs: that is a password-login target for the VTA's vault/proxy-login driver and shares nothing with this but a workspace.

One usability detail worth having

It reports when no provider is present rather than leaving a dead button. Content scripts are registered per granted origin and never reach a tab that is already open, so a first visit after granting needs a reload — which is a confusing five minutes if the page just silently does nothing.

Why this matters beyond the two buttons

Almost everything merged in this run landed unvalidated, and the two defects that reached you — a vault list that would not load, and this login path — were both "no way to drive it locally" problems. This is the cheap end of fixing that: the RP runs locally, the provider API is already public, and the page is a hundred lines.

Next natural extension once the auth family is dispatchable as a Trust Task on the RP: the same page gets a third button that drives the challenge → authenticate flow over TSP.

Zero-dependency, no build step, not wired into CI. 617 workspace tests unaffected; lint clean.

Both RP-login flows — REST SIOPv2 (`login`) and DIDComm (`loginDidcomm`) —
could only be exercised against a real deployment, so in practice they were not
exercised at all. One of them was broken for an unknown length of time as a
direct result: the wallet sent a retired `authenticate` Type URI the control
plane no longer routes, a site-initiated login failed after the user had already
approved the consent prompt, and nothing surfaced it (#139).

This is the missing half of that loop. A page that calls `window.vtaWallet` the
way a relying party does, against a control plane of your choosing — the live
one, or a `did-hosting-control` on localhost — and shows what came back.

It asserts nothing. The point is to make the round-trip observable next to the
wallet's own console, which is where the diagnosis actually happens; a harness
that judged the result would just be a worse unit test with a network
dependency.

Kept out of `server.mjs` deliberately: that is a password-login target for the
VTA's `vault/proxy-login` driver and shares nothing with this but a workspace.

It also reports when no provider is present rather than leaving a dead button —
content scripts are registered per granted origin and never reach a tab that is
already open, so a first visit after granting needs a reload, and that is a
confusing five minutes if the page says nothing.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 merged commit a854d42 into main Aug 29, 2026
3 checks passed
@stormer78
stormer78 deleted the feat/rp-login-test-harness branch August 29, 2026 13:57
stormer78 added a commit that referenced this pull request Aug 30, 2026
…hand (#144)

`proxyLogin` required the *page* to name a vault entry, so a per-site
persona had to be bound in advance through the vault panel and a site
without one dead-ended. The page's only route to an entry id was
`vaultList()` — a second consent prompt that enumerates the user's vault
to the site asking — and on a site with nothing bound it returned an
empty array with nowhere to go from there. In practice that made the
per-site persona a setup step people did not do, which left `login()`
and its holder DID as the path of least resistance.

The wallet now resolves the entry itself, from the origin the browser
attested, and asks the human once when there is nothing bound yet.
`entryId` becomes optional; a page that still supplies one keeps the old
behaviour exactly.

**One prompt, not two.** Choosing the identity a site sees IS the
sign-in approval — it names the site and the identity, which is what the
prompt already claims to do. A picker in front of a second approve
screen would only train the operator to click through both (R7.2).

**The picker bypasses the origin-trust short-circuit, deliberately.**
The first-use branch calls `requestConsent`, not `gatedConsent`. A
"remember this site" tick made against an earlier sign-in meant "log me
in as the identity I already chose for you"; it cannot mean "choose a
new identity for me and bind it silently", because that question has
never been put to the operator. Same reasoning `requestTaskConsent`
already carries: origin trust is not capability trust. Delete an entry
and the picker returns, trusted origin or not.

**The origin match is local and exact.** `vault/list` narrows by
`targetOriginPrefix`, and a prefix is not an origin —
`https://shop.example` is a prefix of `https://shop.example.evil.test`.
Narrowing the set the VTA sends is a bandwidth decision; deciding which
entry is this site's is a security decision, so it happens locally with
`===`. Tested in both directions, plus scheme and port.

**The prompt returns a DID string and nothing else.** Context and
signing key are re-derived in the background from the agent's own
`list-dids` and `derive-signing-key-id`, so a DID the agent does not
host cannot be bound whatever the consent window sends back. A persona
with more than one candidate signing key is refused rather than guessed
— which key mints the id_token is a real choice with no default, and a
wrong guess fails later and opaquely at the VTA — with the operator sent
to the vault panel, which has the key picker.

Entries are bound to `{kind:"webOrigin"}` *and* the RP's DID when the
page named one. The vault panel's did-self-issued form binds a DID
target only, so entries created there stay invisible to an origin
lookup; that is left alone rather than migrated, since nothing is
deployed and the panel is still the place to bind by hand.

The ACL caveat is stated in the prompt, beside the DID, rather than
after the failure: the relying party decides which identities it admits
and nothing this wallet does can add one, so the operator wants the DID
on screen while it is still copyable. A failed first sign-in repeats it
and names the DID. The entry is kept on failure — it is correct, and
deleting it would make the retry-after-enrolment path ask for an
identity all over again.

`login()` (REST SIOP) is untouched and still signs as the holder DID for
every site. That path reads no vault entry at all, so aligning it is a
behaviour change to a working flow and wants its own decision.

Decision logic sits in `first-use-profile.ts`, free of `chrome` so it is
testable; the demo harness gains a `proxyLogin({})` button, since the
whole point of #140 was that a flow nothing exercises stays broken.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
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