Conversation
…mple Opens the tutorial series specified in specs/002-tutorial-series. - docs/tutorials/00-nostr-for-nap.md — keys, the three signers, what NIP-98 signs, and the one thing a signature does not prove. - Fixes four documentation defects: the stale version in README, the two places claiming stepUp() always throws (§6.1 was right, and session.ts mints the token), the two claiming NAP ships no KeyStore implementation, and the three packages with no README. - examples/merchant-app joins the workspace, with one HTTP seam covering init, completion, payload-mismatch rejection, guarded routes, permission denial, step-up, refresh rotation, and cookie logout. - docs/tutorials/01-a-server-you-can-curl.md walks the whole exchange by hand. Every output in it is copied from a real run. Three library findings recorded in CONTEXT.md rather than fixed: the guards expose no principal, NapExpressGuardOptions.clock does not reach the expiry check, and @types/express needs the same dedupe nostr-tools does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the merchant app's frontend and the tutorial built on it. - Vite dev server proxying /auth, /api, /permissions so the browser sees one origin. Both the SameSite=Lax cookie and the NIP-98 audience depend on that, so the server must run with NAP_BASE_URL pointed at the frontend's origin — verified by reading auth_url back out of a live /auth/init. - useNapBootstrap: createNapSession with the callbacks spread whole, resume() on mount behind a loading phase, destroy() on unmount. - SignerPicker renders useNip07()'s 'detecting' tri-state and all four Nip07Error codes distinctly; the switch is exhaustive with no default. - Test now asserts logout clears the cookie under the name it was written with. - Example pins React 19 to match nap-react's @types/react major (CONTEXT.md finding 9: nap-react declares no react peer dependency at all). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Adds a `support` role with no permissions and one `requireRole`-guarded staff route: the case a permission check cannot express, which is the test for whether a role guard is the right tool. - Frontend gains a voucher list and create form gated on the session hook's permissions, with the 403 handled anyway — the hidden button is reachable from a second tab, a stale snapshot, or curl. - Three tests: viewer read/write split, the role guard both ways, and a live revocation biting mid-session because the guards re-read the ACL. The last one is what `aclResolver` in the guard options buys, asserted rather than claimed. - Tutorial output is all from live runs, including the boot failure from a deliberately typo'd permission key. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- `docker-compose.yml` (Postgres 16 on 5433) plus `db/schema.sql`, the DDL the repo does not ship. Derived from the store's queries; verified by running it. - `src/stores.ts` selects in-memory or Postgres off DATABASE_URL, and carries the challenge sweeper. All three stores swap together as a constructor call. - Found and worked around: `pg` returns BIGINT as a string, and `PostgresSessionStore` casts it to `number` without converting — so /auth/session ships `"expires_at":"1787407576"` on Postgres and a number in memory. Comparisons coerce and appear fine; arithmetic concatenates, and the session body is the cross-implementation contract. Pool-scoped INT8 parser in the example, filed as CONTEXT.md finding 10. - Tutorial has the reader lose a session to a restart before fixing it, and states plainly that markExpired() marks but never deletes and sessions are never swept — demonstrated with real row counts. CI unchanged: no Docker, in-memory stores, typecheck covers the wiring. The tutorial says so rather than implying the path is tested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tutorial 05 walks the 900-second wall, `refreshTtlSeconds`, rotation, and
reuse detection from a captured live run of the example. The client side is
the point: neither `nap-client-web` nor `nap-react` contains the string
"refresh", so `refreshLoop.ts` is the sixty lines an integrator has to write,
including the two things that are easy to get wrong — `credentials: 'include'`
so the rotated cookie lands, and never retrying a refresh whose outcome is
unknown, which the server reads as theft and answers by revoking the session.
The example gains `NAP_REFRESH_TTL` / `NAP_SESSION_TTL` knobs so the reader can
watch a session expire in a minute rather than a quarter of an hour, a cookie
`transformBody`, and a test covering a rotation plus the replay that kills the
session.
Guide corrections: §11.3's "no client refreshes for you" bullet omitted that in
cookie mode the token does not reach the browser at all by default, and §5.2
framed `transformBody` as a render optimisation when it is mandatory for any
`nap-client-web` consumer — the bare `{status:'ok'}` body makes `login()` throw
a `TypeError`. Filed as CONTEXT.md finding 11; no library code changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tutorial 06 enforces the `stepUp: true` flag that ticket 03 declared on `stripe:manage`, from the curl transcript through a React component that elevates on the 403 rather than deciding for itself when to prompt. Three findings the guide did not carry, all confirmed against a running server. A step-up mints a *second* session rather than upgrading the first, so a bearer client that keeps its original access token holds an elevation it can never spend — the guard checks the step-up token against the session the request's own credential names. Omitting `registry` from the guard options disables enforcement silently, which is the one piece of step-up wiring that fails quietly rather than loudly. And guard denials reach no `AuditLogger` at all: the whole sequence emitted two records, both `NAP_COMPLETE_SUCCESS`. The consent framing is stated three times over because the feature invites the mistake: step-up is evidence of present key control, and a NIP-46 bunker with pre-granted permissions satisfies it without asking anybody. The "try it" ends by pressing the button twice and getting one prompt. Guide §6.1's account of `stepUp()` is confirmed empirically — no disagreement with ticket 02's fix. The forward-link filenames for 06/07/08 had drifted into two conventions; normalised on the short form the shipped files already use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The example gains a `RemoteSigner` component offering both pairing directions
from one field — a pasted `bunker://` URL, or a `nostrconnect://` URI the page
emits for the user to scan — with optional encrypted persistence so a reload
does not redo the two-device dance. It is offered in both branches of the
signer picker, including the no-extension one, where it is the difference
between a dead end and a login.
`describe()` in the picker now falls through to a NIP-46 taxonomy rather than
collapsing six codes into "something went wrong": `login()` and `stepUp()`
surface whichever signer is behind the session, and only `DECLINED` is a choice
somebody made.
The tutorial's §3 is the reason this is a tutorial and not a paragraph. The
client pubkey is public — in the URI and in the `#p` filter every relay sees —
and NIP-44 conversation keys are ECDH, so anyone can send something that
decrypts. Only the returned secret proves the sender read the URI, and an
`{error}` arriving mid-pairing is unauthenticated: treating it as a decline
hands every relay operator a kill switch on every pairing in flight.
Also honest about what it does not have. The pairing sequences are described
rather than pasted, because a live bunker on a live relay is not reproducible
from a repository, and the tutorial says so up front rather than presenting
invented output as a transcript.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes the series' signer arc with in-page key custody, wired the way the
RFC requires rather than the way apps reach for by default.
Example app:
- `storage.ts` — one `createWebCryptoKeyStore` and one
`createSignerPreferenceStore`, shared by the screens and the session.
- `KeyLogin.tsx` — enrol an nsec under a passphrase, or unlock a stored
one. Never writes plaintext key material (RFC §1181).
- `LockScreen.tsx` — a total switch over `lockRecovery()`. The three arms
need three affordances and two cannot satisfy the other's.
- `autoLock` + `keyStore` wired together, which is the combination that
avoids the bricked session `createNapSession` throws to prevent.
- Signer preference across reloads, honouring the promise tutorial 02
made — with `resume({ verifyIdentity: true })` on every restore path,
because the cookie outlives the page and the signer does not.
- `SignerChoice` carries signer, kind and verifyIdentity together so they
cannot drift apart.
Tutorial covers: why this is last, encryption at rest, the wiring throw,
the three-way lock recovery, why nothing unlocks on the user's behalf,
cross-tab lock/logout, and the ceiling — a hostile script on the origin
defeats all of it, because the app's own code must be able to decrypt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes Phase 4 of the integration guide's build order against the example app, and says plainly what that does and does not mean. Example app: the rate limiter, both outstanding-challenge caps, and the router body limit are now spelled out rather than inherited, because the numbers are the thing you are supposed to size. Adds an opt-in `trustProxy` — it decides `req.ip`, which the per-IP cap counts on, and `req.protocol`, which a request-derived audience resolver reads. Adds the DELETE job tutorial 04 said you would have to write, on its own hourly cadence with separate horizons for challenges and sessions. Two integration tests where the hardening is observable over HTTP: the 413 for an oversize /auth/init body, and a 429 with a Retry-After from a tightened limiter. The tutorial covers what the caps do differently (arrival vs. concurrency — both answer 429, and only the audit log's `cap` field tells them apart), revocation timing, cookie attributes and the proxy, the multi-domain audience allowlist and why it has no default, nostr-tools deduping, clock skew, and a closing table of what the example app still is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…alBaseUrl createRequestDerivedBaseUrlResolver returns a (req) => string, which is getExternalBaseUrl's shape, not AudienceResolver's. audienceResolver is the separate full-URL option for a rewriting gateway; the tutorial now says so rather than conflating them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One page instead of a second example application, per ADR 0001: the difference between the two adapters is about twenty lines, and a parallel Fastify example would double the surface that has to stay correct. Opens with the whole substitution as a table, then works through mounting, the raw body, the body limit, guards, the permissions plugin, cookie mode, and audience/proxy/rate-limiter wiring — each keyed to the tutorial section it substitutes into. Names the option differences explicitly rather than leaving them to be discovered: bodyLimitBytes takes bytes and not '1kb', permissionsFastifyPlugin is a factory where createPermissionsRouter is not, guards attach as preHandler, and the cookie attribute type differs. Two things worth the reader's attention in the other direction. Express's global-express.json() trap does not exist here, because the plugin's parser is scoped to its encapsulation context — but wrapping the registration in fastify-plugin makes NAP's 1 kB raw-body parser the whole app's global JSON parser. And Fastify's own body-limit default is 1 MB, not Express's 100 kB, so the adapter's 1 kB default is protecting you from ten times as much. The §1–§7 wiring was compiled against the workspace under npm run typecheck. Everything in nap-server — the rate limiter, both challenge caps, the ACL resolver, step-up — needs no translation at all, which is the point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Collects the WebAuthn precedent NAP already follows — currently scattered across the RFC, the integration guide, the best-practices file and a doc comment in nap-server/src/audience.ts — into one page, and states the three places NAP is permanently weaker: no clone detection, no multi-credential recovery, no signed UP/UV bit. Standalone by construction. Nothing links to it yet, so dropping it is deleting one file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Links to RFC Appendix D as the comparison proper rather than forking it. Adds what Appendix D lacks: the "you have OAuth today" angle, adapting guide §10's five phases to an OAuth incumbent — including the account linking decision (IdP `sub` vs hex pubkey) that a NIP-98 incumbent never has to make, scopes-in-the-token vs the login-time ACL snapshot, and what an authorization server was doing for you that NAP does not. Leads with which question each protocol answers, and says plainly that they are not substitutes where delegation is the requirement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- docs/tutorials/README.md lists all ten tutorials with what each one gets you, routes the Nostr-literate reader to guide §1–§3, and links the two comparison pages as one line each so dropping them is a one-line delete. - README gains a "Start here" section pointing at the series, and states that examples/merchant-app consumes the packages from the workspace. - Guide §0.4's phase table gains a Tutorial column, plus a note on the three things the phases do not order (refresh, step-up, non-NIP-07 signers). Ten sections gain a "Walk it:" pointer into the tutorial that walks them. - Guide version claim corrected 0.3.0 -> 0.10.1. Every relative link in docs/, README.md and the package READMEs verified to resolve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Spec for using an Imani-issued Cashu voucher as the source of a NAP session's roles and permissions, replacing the ACL store row. Changes authorization only; NIP-98 authentication is untouched. Core design: the voucher is P2PK-locked to a key, and the completion's NIP-98 event must be signed by that same key. Mint URL is mandatory (keyset is not a locator, liveness is mint-local, trust is per-mint) and the mint allowlist follows the audience-allowlist rules. Draft for review. Six open questions, the load-bearing one being how VOUCHER and P2PK secret kinds compose against an unmodified mint.
scripts/doccheck.py verifies that every relative link resolves, every backtick-quoted type name matches a real Java or TypeScript type, and every environment variable bound in application config appears in the configuration docs. It exits non-zero so it can gate a commit. Where present, .doccheck-allow declares names that are deliberately not types in this repository: third-party and platform types, and vocabulary from design proposals that are not yet built.
Three fixes found while extending the checker across the rest of the stack: - Index .ts and .tsx declarations and exported React components. Several repos ship a TypeScript client whose types are legitimately named in docs; without this the checker reported them all as unknown. - Skip plans/, specs/, superpowers/, and archive/ directories. Those documents quote link lines destined for other files, whose relative paths are correct only from the target, so checking them reports the quoting rather than a broken link. - Extend the built-in skip list with JDK, Spring, browser, and node types commonly named in prose.
The link checker special-cased http, https, and mailto. A nostr: URI in a README was therefore resolved as a relative path and reported broken. Any scheme before the first slash now counts as external. Verified against a fixture: scheme URIs skip, a genuinely broken relative link is still reported and still exits non-zero.
A superseded checkout keeps its documentation as written; repairing its links would imply the tree is maintained. A repository can now declare 'doccheck: skip-links' in .doccheck-allow to record that decision. Opt-in only, verified against a fixture: without the marker a broken link is still reported and still exits non-zero.
The class check indexes sibling repositories because docs legitimately cite types
in them. It guarded against a single-repo checkout with a MIN_TYPES=50 threshold,
which is unsound: a repository with its own sources easily exceeds 50 while still
lacking every sibling, so in CI the check ran against an incomplete index and
reported cross-repo references as unknown. Simulating a CI clone confirmed it:
cashu-mint alone produced 35 false unknowns.
Detect the case directly instead, by asking whether any sibling repository is
present, and handle the partial workspace between the two extremes:
single repo class check skipped; links and config still checked
partial unresolved names reported but not failed, since a name may
live in a repo that is not checked out
full workspace class check enforced
Verified in all three modes, and against a fixture with a genuinely broken link
and a fake class name, which still fails and still exits non-zero.
This was referenced Sep 24, 2026
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.
Documentation only.
nappublishes no Maven artifact, so there is no version to bump.What is here
reachable rather than only present.
getExternalBaseUrl, aFastify appendix, and a "before you ship" section.
scripts/doccheck.py, the shared documentation checker.Verification
doccheck.pyrun locally. GitHub Actions runners are not being assigned org-wideat the moment.