Repository navigation
Conversation
DEV-166 and DEV-169. Both doc-only; they overlap, so they land together. RFC gets a new §7.1 under Security Objectives holding the four structural gaps: origin substitution, the absent user-presence signal, clone detection, and account recovery. §7's "MITM / forwarding" row overclaimed — audience binding stops a proof being forwarded, not a hostile origin obtaining a correctly-addressed one — so it now points at §7.1. The five dead ends are recorded there so nobody re-derives them, along with the conclusion: the refusal has to come from the signer, which makes an audience-enforcing NIP-46 bunker the phishing-resistant path and NIP-07 the weak one. §10.3 said step-up "proves key control, at this moment", which is true and insufficient. With a remembered NIP-07 grant or a pre-permissioned bunker the whole ceremony runs with nobody present, so stepUp:true caps a stolen token and does nothing about a hostile page that already has signer access. Also forbids the obvious wrong fix — a presence tag the page would write itself. §14.3's "optional risk scoring on drift" was unimplementable as written: the RFC persists no per-principal state to drift from. Says so, and what a deployment would have to add first. §28.5 stays scoped to key custody and cross-refs §7.1 rather than absorbing protocol-level threats into a table about encryption at rest. Integration guide gets §9.8 as the operator-facing version, four new rows in §9.7, and the consent caveat wherever stepUp is described (§3.3, §6.1). Best practices gets the WebAuthn §13.5.8 code-injection mitigations we were missing (CSP, third-party script, no user content in scope) and the clickjacking asymmetry: extension and bunker prompts are browser chrome, the in-page passphrase prompt is your own DOM. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… audience allowlist
Three findings from the WebAuthn Level 3 review, one branch.
getSignerCapabilities() (DEV-167). detectNip07Provider() answers only "is
window.nostr here", so a login screen built on it reports "no signer found" on a
desktop that could pair a bunker or take an nsec — the mistake WebAuthn replaced
with getClientCapabilities(). The new call returns { nip07, nip46, localKey };
only nip07 is detected, because the other two are not browser features but things
the bundle contains, so the app declares them. detectNip07Provider() is unchanged
and still exported — it returns the provider createNip07Signer() needs.
Clear the signer preference on a terminal failure (DEV-168). Nothing cleared it
when the server stopped accepting an identity, so a page kept offering a login
that 401s forever. createNapSession() now takes the store and clears it on a
terminal /auth/init or /auth/complete failure and when the identity guard
terminates; it never writes it, since only the app knows which kind of signer it
built. The rule is "terminal" (any 4xx but 429), not "unknown npub" — §10.1 and
§15 make every auth failure the same uniform 401, so the client is never told
which it was. Deliberately not cleared on logout(), on a resume() that 401s, or
on anything a retry fixes. AuthRequestError carries { phase, status, terminal }
for callers that want to branch themselves.
Require a host allowlist for the request-derived audience (DEV-170). BREAKING.
createRequestDerivedBaseUrlResolver() read Host raw and contained no trust
policy, which let a request header choose the value every NIP-98 proof is checked
against — exactly what WebAuthn L3 §13.5.9 makes it normative for an RP not to
do. It now takes a required allowlist and throws at wiring time without one, in
both adapters. Entries are exact hosts, optionally scheme-pinned
(https://api.example.com, which takes X-Forwarded-Proto out of it) and optionally
subdomain wildcards (*.example.com, opt-in per entry because §13.5.8's default is
no). The matching lives in nap-server's createAudienceHostAllowlist() so neither
adapter owns a second copy of the trust policy.
nap-java needs no matching change: it has no request-derived resolver, only an
AudienceResolver bean or a pinned nap.external-base-url. Nothing on the wire
moved.
BREAKING CHANGE: createRequestDerivedBaseUrlResolver() now requires a host
allowlist argument. Pass the hosts you answer on, or switch to a pinned constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All nine packages share one version, so they move together. Breaking, but pre-1.0, so a minor: createRequestDerivedBaseUrlResolver() now requires a host allowlist and throws at wiring time without one. Also adds getSignerCapabilities(), AuthRequestError, and NapClientOptions.signerPreference. Nothing on the wire moved — nap-java interoperates unmodified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There is no build step, so the consumer's compiler compiles this repo's source. Since 0.9.0 that source uses the generic Uint8Array<ArrayBuffer> in webCryptoSecretStore.ts, which TypeScript models only from 5.7. Below that floor the failure is TS2315 "Type 'Uint8Array' is not generic", pointing into node_modules — a compiler floor that reads like a bug in NAP. Local typecheck cannot catch it: this repo devDepends ^5.7.2. Found by vendoring 0.10.0 into a consumer still on 5.6.3. Declared as an optional peer dependency on every package, so npm reports it at install time. Optional because the case worth failing on is present-but-older; a non-optional peer auto-installs a compiler into consumers that pinned their own. No behaviour change, no wire change. README gains a Requirements section and CLAUDE.md a trap, since adopting newer TypeScript syntax raises the floor for every consumer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-allowlist feat!: signer capabilities, preference clearing, audience host allowlist (0.10.1)
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.
Promotes
developtomaster. Four commits, two releases, one breaking change.Already merged and CI-green on
developas #10; tagsv0.10.0andv0.10.1are reachable from this history.createRequestDerivedBaseUrlResolver()requires a host allowlistIt read
Hostraw and contained no trust policy, which let a request header choose the audience every NIP-98 proof is checked against — the thing WebAuthn L3 §13.5.9 makes it normative for a relying party not to do.refreshTtlSecondschecks.*.example.comis a per-entry opt-in and does not match the apex.https://api.example.com) — an allowlist over hosts alone leavesX-Forwarded-Protoable to downgrade the audience.@imani/nap-server'screateAudienceHostAllowlist().Migration: pass your hosts,
createRequestDerivedBaseUrlResolver(['api.example.com']), or use the pinned constantgetExternalBaseUrl: () => 'https://api.example.com'.Also in this promotion
getSignerCapabilities()—{ nip07, nip46, localKey }. Onlynip07is detected; NIP-46 and an in-page key are things your bundle contains, so the app declares them.detectNip07Provider()is unchanged and still exported.NapClientOptions.signerPreference, cleared on a terminal/auth/initor/auth/completefailure and on identity termination, never onlogout()or aresume()401. PlusAuthRequestError { phase, status, terminal }, where terminal is any 4xx except 429.webCryptoSecretStore.tshas used the genericUint8Array<ArrayBuffer>since 0.9.0. Every package now declares it as an optional peer dependency so npm reports it at install time, instead ofTS2315surfacing from insidenode_modules.bafbeca).Verification
npm test344 tests / 31 files,npm run typecheckclean, CI green on Node 20.19.0 and 22.x.Both TypeScript consumers verified against this code:
dalia-chat-clientre-vendored 0.8.0 → 0.10.1 (build clean, 312 tests),imani-walletpath-aliased so it needs no pin (build clean, 343 tests).bottin-admin-ui/bottin-client-uiare Java consumers ofnap-springand are unaffected.Cross-implementation
Nothing on the wire moved —
nap-javainteroperates unmodified and needs no code change, since it ships no request-derived resolver. ItsAudienceResolverjavadoc now carries the allowlist and scheme-pinning guidance (tcheeric/nap-java#12, merged).🤖 Generated with Claude Code