Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ All packages in this workspace share a single version.

## [Unreleased]

## [0.13.0] - 2026-10-28

Minor: one additive feature, no behaviour change for existing integrations. Every package
moves to 0.13.0 together, because the workspace shares one version, but only
`@imani/nap-client-web` has new code. In 0.x a caret range does not cross a minor, so a
consumer on `^0.12.0` must change it to `^0.13.0` to get this release.

### Added

- **Extension 0002, cross-origin signer** (`@imani/nap-client-web`, tcheeric/nap#54). With
`connectCrossOrigin()`, a sister origin opens the key origin's signer page as a popup and
gets a scoped, short-lived signing grant. The returned `grant.signer` is an ordinary
`SessionSigner` for `createNapSession`. On the key origin, `mountCrossOriginSigner()`:
- answers only its opener, and only for origins in an explicit policy, using the
browser-reported origin;
- signs only kinds and `u`-tag audiences inside the grant, with `created_at` near now;
- prompts through `onSign` for every completion proof after the first (step-up);
- revokes every grant on lock, logout or identity change.

`createStorageConsentStore()` remembers approvals. Client-only: servers and `nap-java` are
unchanged. Spec: `docs/extensions/0002-cross-origin-signer.md`. An opt-in Chromium e2e runs
with `NAP_BROWSER=1`.

## [0.12.0] - 2026-10-08

The packages are publishable. Each compiles to ESM plus `.d.ts` in `dist/` and is released to
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ published manifest. A new consumer of a sibling inside this repo must select it

**The registry scope is one variable.** Code imports `@imani/nap-*` everywhere. `npm run
release` publishes as `NAP_SCOPE` (default `@398ja`), writing sibling dependencies as npm
aliases (`"@imani/nap-core": "npm:@398ja/nap-core@^0.12.0"`) so the import names survive.
aliases (`"@imani/nap-core": "npm:@398ja/nap-core@^0.13.0"`) so the import names survive.
Never hand-edit a package name to change scope. See `RELEASING.md`.

## Repository structure
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ the root `package.json` is the single place to read it.
> npm alias:
>
> ```bash
> npm install @imani/nap-client-web@npm:@398ja/nap-client-web@^0.12.0 nostr-tools
> npm install @imani/nap-client-web@npm:@398ja/nap-client-web@^0.13.0 nostr-tools
> ```
>
> See [RELEASING.md](RELEASING.md) for the scope and the release command. Inside this
Expand All @@ -40,7 +40,7 @@ Already know Nostr and want the reference instead? Start at
| `@imani/nap-core` | Protocol types, hashing, base64/hex codecs, header parsing, NIP-98 completion validation. Runs in browser and server. |
| `@imani/nap-server` | `createNapServer()` — challenge issuance, retry-safe completion verification, refresh rotation, ACL resolution, in-memory stores, rate limiter, metrics. |
| `@imani/nap-client-http` | Request building for `/auth/init` and `/auth/complete`. |
| `@imani/nap-client-web` | Browser session lifecycle: `createNapSession()`, NIP-07 detection and signers, identity guard, idle lock, re-unlock, cross-tab sync. |
| `@imani/nap-client-web` | Browser session lifecycle: `createNapSession()`, NIP-07 detection and signers, the cross-origin signer (extension 0002), identity guard, idle lock, re-unlock, cross-tab sync. |
| `@imani/nap-client-nip46` | NIP-46 remote-signer (`bunker://` / `nostrconnect://`) `SessionSigner`, with encrypted reconnection. **Opt-in** — install it only if you want remote signers; it is the only package that talks to relays. |
| `@imani/nap-react` | `NapProvider`, `useNapSession()`, `useReunlock()` over `nap-client-web`. |
| `@imani/nap-adapter-express` | `createNapExpressRouter()`, guards, raw-body-safe JSON parser. |
Expand Down
8 changes: 4 additions & 4 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ The scope the packages are **published** under is a separate choice, made in one

| `NAP_SCOPE` | Published as | Sibling dependency in the published manifest |
| --- | --- | --- |
| `@398ja` (default) | `@398ja/nap-core`, ... | `"@imani/nap-core": "npm:@398ja/nap-core@^0.12.0"` |
| `@imani` | `@imani/nap-core`, ... | `"@imani/nap-core": "^0.12.0"` |
| `@398ja` (default) | `@398ja/nap-core`, ... | `"@imani/nap-core": "npm:@398ja/nap-core@^0.13.0"` |
| `@imani` | `@imani/nap-core`, ... | `"@imani/nap-core": "^0.13.0"` |

With `@398ja` the published packages depend on each other through npm aliases, so the
`@imani/nap-*` import names keep working with no rewrite of the JavaScript. A consumer
declares the same aliases:

```json
"@imani/nap-client-web": "npm:@398ja/nap-client-web@^0.12.0",
"@imani/nap-react": "npm:@398ja/nap-react@^0.12.0"
"@imani/nap-client-web": "npm:@398ja/nap-client-web@^0.13.0",
"@imani/nap-react": "npm:@398ja/nap-react@^0.13.0"
```

and npm installs one copy of each: the consumer's alias and the transitive one resolve to the
Expand Down
18 changes: 18 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Upgrading

## 0.12.0 to 0.13.0

Nothing to change in your code. 0.13.0 adds the cross-origin signer (extension 0002) to
`@imani/nap-client-web`. Existing sessions, signers and servers behave exactly as before.

**Bump the range.** In 0.x, `^0.12.0` does not match 0.13.0, so update every `@398ja/nap-*`
alias together (the packages share one version):

```json
"@imani/nap-client-web": "npm:@398ja/nap-client-web@^0.13.0",
"@imani/nap-react": "npm:@398ja/nap-react@^0.13.0"
```

**Only if you adopt the cross-origin signer:** follow
[extension 0002](./docs/extensions/0002-cross-origin-signer.md) §8 for the signer page headers.
The signer page must not send `Cross-Origin-Opener-Policy: same-origin`, and the relying page
must not send `same-origin`.

## 0.11.0 to 0.12.0

No runtime behaviour changes. What changes is how you get the packages.
Expand Down
40 changes: 39 additions & 1 deletion docs/NAP-INTEGRATION-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1225,7 +1225,7 @@ existing resolver as `fallback` rather than wiring two servers.

Ten npm-workspace packages sharing one version. Each ships compiled ESM and
`.d.ts` from `dist/`; imported as `@imani/nap-*`, published as `@398ja/nap-*`
(install with an alias, e.g. `"@imani/nap-core": "npm:@398ja/nap-core@^0.12.0"`;
(install with an alias, e.g. `"@imani/nap-core": "npm:@398ja/nap-core@^0.13.0"`;
see `RELEASING.md`). `nostr-tools`, `@noble/*` and `react` are peer
dependencies.

Expand Down Expand Up @@ -1979,6 +1979,44 @@ path whose prompt is **your own DOM**. An extension or bunker prompt is browser
OS chrome and cannot be overlaid or mimicked by a framing attacker; a passphrase
field can. Serve the pages that render it with `frame-ancestors 'none'`.

**Cross-origin signer (extension 0002)** (`packages/nap-client-web/src/crossOriginClient.ts`,
`crossOriginSigner.ts`).

Use it when a sister site should sign in with a key that lives on another origin, e.g. a shop
on `shop.example` and a wallet on `wallet.example`. The wallet mounts a signer page, and the shop
opens it as a popup. The wallet checks the shop's real origin (from the browser) and the `u`
tag of every event before signing. The key never reaches the shop.

```ts
// wallet.example/nap/signer
const handle = mountCrossOriginSigner({
policy: { 'https://shop.example': { kinds: [27235], audiences: ['https://shop.example'] } },
signer,
ensureUnlocked: () => ui.unlock(),
onConsent: (request) => ui.confirm(request),
onSign: (request) => (request.reason === 'completion' ? ui.confirmStepUp(request) : Promise.resolve(true)),
consentStore: createStorageConsentStore(),
});

// shop.example, inside a click handler (window.open needs the gesture)
const grant = await connectCrossOrigin({ keyOrigin: 'https://wallet.example', kinds: [27235], audiences: [location.origin] });
const session = createNapSession({ baseUrl: location.origin, signer: grant.signer });
await session.login();
```

Three things bite:

- **COOP.** The signer page must not send `Cross-Origin-Opener-Policy: same-origin` (or
`same-origin-allow-popups`), and the relying page must not send `same-origin`: either one
severs `window.opener` and the popup never answers (`NOT_AVAILABLE`).
- **User gesture.** Call `connectCrossOrigin` directly from a click, before any `await`, or the
popup is blocked (`POPUP_BLOCKED`).
- **Step-up prompts.** The first completion proof rides the connection consent. Later ones
(step-up, re-login) go through `onSign`, and are refused without it.

The full rules and threat model are in
[extension 0002](./extensions/0002-cross-origin-signer.md).

### 6.3 Idle lock, shutdown, and cross-tab sync

> **Walk it:** [tutorial 08 §5–§7](./tutorials/08-in-page-keys.md) drives the lock, the passphrase re-unlock, and the cross-tab broadcast in a browser.
Expand Down
7 changes: 7 additions & 0 deletions docs/NAP-v2-RFC.md
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,13 @@ Each of those should be specified as an extension, not implied by the core profi
| Extension | Status | Relates to |
| --- | --- | --- |
| [0001 — Voucher-Bound Authorization](./extensions/0001-voucher-bound-authorization.md) | Draft, partially implemented | §15, and items 1 and 5 above |
| [0002 — Cross-Origin Signer](./extensions/0002-cross-origin-signer.md) | Draft, implemented (`nap-client-web`) | §7.1, §10.3, §28.2; client-only |

Extension 0002 lets a sister origin get a session from a key held on another origin,
through a popup that checks the browser-reported caller origin and the `u` audience before
signing. That is the signer-enforced audience binding §7.1 asks for, reached without a
bunker. It is client-only: the server receives an ordinary §11 proof, so nothing in the core
profile or in `nap-java` changes.

Extension 0001 lets an Imani-issued Cashu voucher supply a session's roles and
permissions in place of a stored ACL row. It changes **authorization only** —
Expand Down
Loading
Loading