[← All NAPs](https://napplet.run/protocol/)

NAP-LINK / PR #53

# Shell-Mediated Link Opening

Open PR

This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it.

**Shipped implementation: supported**

The small link-opening binding has focused regression coverage; navigation authorization stays with the host.

Implementation proposal: [in-review ↗](https://github.com/napplet/naps/pull/53), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change.

[Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html)

Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/100e5c939ae6b1ea82cc58587c6da45a1cf8a760/naps/NAP-LINK.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/53/files).

## NAP-LINK

### Shell-Mediated Link Opening

`draft`

**NAP ID:** NAP-LINK **Domain:** `link` **Depends:** none. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.link`; domain presence signals availability.

### Description

NAP-LINK lets a sandboxed napplet ask the shell to open an external URL for the user. The shell owns the navigation, policy, prompting, and browser context. The napplet never gets direct navigation authority, network access, opener access, or fetched bytes.

This is for user navigation, including links to pages that cannot be opened correctly inside the napplet sandbox because of Cross-Origin-Opener-Policy (COOP), frame, popup, or cross-origin restrictions. It is not a fetch API. Use NAP-RESOURCE when the napplet needs bytes returned. Parsers that turn external URLs into clickable UI SHOULD route those clicks to `link.open`.

### API Surface

| Operation | Parameters | Result | Wire |
| --- | --- | --- | --- |
| `open` | `url` (`text`), optional `options` (`LinkOpenOptions`) | `LinkOpenResult` | `link.open` / `link.open.result` |

#### Schemas

`LinkOpenOptions`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `label` | `text` | no | Human-readable prompt text supplied by the napplet. |

`LinkOpenResult`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | `"opened"` or `"denied"` | yes | Whether the shell accepted or refused the navigation request. |
| `error` | `text` | no | Reason when `status` is `"denied"`; same value as the wire field; absent when `status` is `"opened"`. |

**`open(url, options?)`** -- Requests that the shell open `url` for the user. `url` MUST be an absolute URL. `label` is optional human-readable text for the shell prompt; it is not trusted policy input. Resolves with a `LinkOpenResult`: `status` is `"opened"` when the shell accepted the request and handed it to the user agent, or `"denied"` when user or shell policy refused it; `error`, when present on the wire, is passed through unchanged.

### Wire Protocol

`link.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`).

| Type | Direction | Payload fields |
| --- | --- | --- |
| `link.open` | napplet -> shell | `id`, `url`, `options?` |
| `link.open.result` | shell -> napplet | `id`, `status`, `error?` |

Key design notes:

-   `id` correlates the request and result.
-   `link.open` is user navigation, not resource retrieval.
-   The shell chooses the final target: new tab, external browser, in-shell browser, native handler, or denial.
-   The shell MUST NOT expose `window.opener` or equivalent authority back to the napplet.

#### Examples

**Open an external page:**

```
-> { "type": "link.open", "id": "l1", "url": "https://example.com/post/123", "options": { "label": "Read post" } }
<- { "type": "link.open.result", "id": "l1", "status": "opened" }
```

**Denied by policy:**

```
-> { "type": "link.open", "id": "l2", "url": "file:///etc/passwd" }
<- { "type": "link.open.result", "id": "l2", "status": "denied", "error": "unsupported-scheme" }
```

#### Error Handling

Result messages MAY include `error` when `status` is `"denied"`. Common errors are `"invalid-url"`, `"unsupported-scheme"`, `"blocked-by-policy"`, and `"user-denied"`.

### Shell Behavior

-   The shell MUST validate `url` before opening it.
-   The shell MUST reject relative URLs.
-   The shell MUST reject `javascript:`, `data:`, `blob:`, `file:`, and other local or code-bearing schemes.
-   The shell MUST respond to every request with a result message carrying the same `id`.
-   The shell SHOULD open web links in a context outside the napplet sandbox.
-   The shell SHOULD use `noopener` or an equivalent opener-isolation mechanism.
-   The shell MAY prompt the user before opening a link.
-   The shell MAY persist user choices per napplet, per hostname, or per URL.
-   The shell MAY support non-web schemes such as `nostr:` or `mailto:` by policy.

Prompt choices MAY include:

-   allow once
-   allow all links from this napplet
-   allow links to this hostname from this napplet
-   deny once
-   deny all links from this napplet

### Security Considerations

-   Opening a link is a user-visible action. Shells SHOULD require a user gesture or prompt unless policy already allows the request.
-   Prompts SHOULD show the napplet name, stable napplet identifiers, author when known, and destination URL or hostname.
-   Shells SHOULD rate-limit denied or repeated link requests to prevent prompt spam.
-   Shells SHOULD treat IDN, punycode, redirects, and lookalike hostnames as phishing risks in prompt UI.
-   `label` is napplet-supplied text. Shells MUST NOT use it as proof of destination or intent.
-   NAP-LINK does not grant direct network access. A napplet that needs direct fetch, WebSocket, SSE, or custom headers needs a different capability.

### Implementations

-   (none yet)

### Changelog

-   `88a3431` - Introduced NAP-LINK as a shell-owned external link opener for sandboxed napplets.
    
-   `b40b51e` - Adopted injected-domain availability and linked the current upstream web binding.
    
-   `59f3090` - Exposed optional denial reasons in the `open` result.
    

[Join the discussion · PR #53 ↗](https://github.com/napplet/naps/pull/53)[Read proposed changes ↗](https://github.com/napplet/naps/pull/53/files)

On this pageSpecification metadata

Capability domain

`link`

Repository state

Open PR

Registry status

Draft

Document marker

draft

Declared dependencies

None declared in the preamble

Source revision

`100e5c939ae6`

[View source on GitHub ↗](https://github.com/napplet/naps/pull/53/files)
