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

NAP-CONNECT / PR #19

# User-Gated Direct Network Access

Draft PRDeferred upstream

The registry marks NAP-CONNECT as deferred. This proposal is dormant and should not be treated as an available protocol capability.

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

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

## NAP-CONNECT

### User-Gated Direct Network Access

`draft`

**NAP ID:** NAP-CONNECT **Domain:** `connect` **Web binding (NIP-5D):** `window.napplet.connect` · `shell.supports("connect")` **Parent:** NIP-5D

### Description

NAP-CONNECT provides sandboxed napplets with user-gated direct network access (`fetch`, `WebSocket`, `SSE`, `EventSource`) to a pre-declared list of origins. Origins are declared at build time via `["connect", "<origin>"]` tags in the NIP-5A manifest. The host shell reads those tags, prompts the user for approval at first load of a given `(dTag, aggregateHash)`, persists the decision, and emits a runtime Content Security Policy whose `connect-src` directive contains the approved origin list. The user's decision is expressed through the HTTP-response CSP the shell serves with the napplet's HTML — not through any postMessage traffic.

A napplet that declares any `connect` tag takes on the posture defined in `NAP-CLASS-2.md`. A napplet that declares no `connect` tags takes on the default posture defined in `NAP-CLASS-1.md`. This NAP does not redefine those class postures — see `NAP-CLASS-1.md` and `NAP-CLASS-2.md` for CSP shapes, consent-flow MUSTs, grant-persistence semantics, residual-meta-CSP refusal requirements, and revocation responsibilities. NAP-CONNECT defines the manifest-tag shape, origin format, canonical aggregateHash fold, runtime API, and capability advertisement that feed into the class determination; the postures themselves are owned by the sub-track documents.

NAP-CONNECT has **no postMessage wire protocol**. Grants are expressed entirely through the runtime CSP the shell emits in the HTTP response for the napplet's HTML, plus a shell-injected discovery meta tag (`<meta name="napplet-connect-granted" content="...">`) read synchronously by the napplet shim at install time. There are no `connect.*` envelopes in either direction. Implementers and readers looking for a wire message in this spec will not find one — the absence is deliberate.

### Motivation

The sandbox model inherited from NIP-5D (`sandbox="allow-scripts"`, no `allow-same-origin`) combined with a restrictive baseline CSP means napplets cannot, by default, reach the network directly. NAP-RESOURCE partially fills the gap with a shell-mediated single-`Blob` primitive (`resource.bytes(url) → Blob`): read-only, shell-visible, policy-enforced at the URL layer, with MIME byte-sniffing and SVG rasterization baked in. That primitive covers avatars, static assets, one-shot byte fetches, and bech32 resolution — a large fraction of napplet use cases.

What NAP-RESOURCE cannot express: `POST` / `PUT` / `PATCH` methods, WebSocket, Server-Sent Events, streaming responses, custom request headers, long-lived connections, and any third-party library that calls `fetch` or opens a socket directly. For those use cases, the napplet needs direct browser-level network access to a specific known host. Direct network access in a sandboxed iframe requires explicit CSP relaxation, which requires user consent — because the shell cannot see or filter traffic that flows directly from the iframe to an approved origin. NAP-CONNECT is the minimum-viable protocol for that consent.

Authors SHOULD default to NAP-RESOURCE for everything it can express; NAP-CONNECT exists specifically for the cases where the resource NAP cannot. The two NAPs are complementary, not competing — most non-trivial napplets will use both.

### Non-Goals

-   **Per-origin partial grants.** v1 is all-or-nothing: approving the prompt approves every declared origin. A future revision MAY add partial grants.
-   **Wildcard subdomains.** `https://*.example.com` is not supported. Each subdomain is a separate origin requiring its own `connect` tag and its own line in the consent prompt.
-   **Quota or rate-limiting on granted traffic.** The browser provides no hook for a shell running in a parent frame to inspect or rate-limit traffic from a sandboxed child frame's direct network I/O. A service worker could mediate the traffic, but the napplet's `sandbox="allow-scripts"` iframe forbids service-worker registration.
-   **Audit logging of individual network calls.** Same reason: the browser enforces CSP transparently to the shell. The shell sees no per-request trace.
-   **A postMessage wire protocol.** NAP-CONNECT is expressed through manifest tags, CSP, and a discovery meta tag. No `connect.request`, no `connect.approve`, no `connect.granted` envelope exists. Adding one would contradict the architectural decision documented here and in NAP-CLASS-2.md.
-   **Shell visibility into post-grant traffic.** Once the user approves an origin, the shell has no browser-level mechanism to observe, filter, or intercept subsequent traffic between the napplet and that origin. This is the fundamental tradeoff of NAP-CONNECT versus NAP-RESOURCE; it is not a defect to be mitigated but a property to be disclosed clearly in the consent prompt.

### Architecture Overview

Three moving parts, and nothing else:

-   **Napplet build.** The napplet author declares required origins through the napplet build tool's `connect: string[]` option. The build tool normalizes each origin (lowercase scheme and host, Punycode for IDN, strip default ports, reject malformed input), emits one `["connect", "<normalized-origin>"]` tag per origin into the signed NIP-5A manifest, and folds the normalized origin set into `aggregateHash` via a synthetic `connect:origins` xTag entry. Any change to the origin set — addition, removal, reorder after normalization — produces a different `aggregateHash` and therefore a different grant key.
    
-   **Shell runtime.** The shell reads the manifest's `connect` tags, validates each origin against the format rules, and looks up grant state keyed on `(dTag, aggregateHash)`. On first load of a new key: prompt the user, persist the decision. On subsequent loads: reuse the persisted decision. On approval, emit a runtime CSP whose `connect-src` directive contains the approved origins and inject `<meta name="napplet-connect-granted" content="<space-separated-origins>">` into the served HTML. On denial, fall back to the posture defined in `NAP-CLASS-1.md` (the shell emits `connect-src 'none'` and the injected meta tag is empty or absent). Grant-persistence and consent-flow details are owned by `NAP-CLASS-2.md`.
    
-   **Napplet runtime.** The napplet shim reads the discovery meta tag synchronously at install time and populates `window.napplet.connect` with `{ granted, origins }`. Napplet code branches on `window.napplet.connect.granted`: when `true`, call `fetch` / open `WebSocket` / `EventSource` directly against approved origins; when `false`, fall back to NAP-RESOURCE for what the resource NAP can express, or degrade the affected feature gracefully. The shim never waits for a wire message — grant state is known at install time.
    

### Posture Citation

A napplet declaring any `["connect", "<origin>"]` manifest tag takes on the posture defined in `NAP-CLASS-2.md`. A napplet with no `connect` tags takes on the default posture defined in `NAP-CLASS-1.md`. NAP-CONNECT does NOT redefine those postures: this NAP specifies only the manifest-tag shape, the origin format, the canonical aggregateHash fold, the runtime API, and the capability advertisement that feed the class determination. The concrete CSP directive shapes, the consent-prompt MUSTs, the shell responsibilities at serve time, the grant-persistence semantics, the residual-meta-CSP refuse-to-serve requirement, and the revocation UX are owned by `NAP-CLASS-2.md` — readers looking for any of those details MUST follow the citation rather than treating this section as a substitute. For the default-deny posture that a napplet without `connect` tags receives (and that a napplet whose user denied the consent prompt is downgraded to), see `NAP-CLASS-1.md`.

### Manifest Tag Shape

Origins are declared in the NIP-5A manifest using one tag per origin:

```
["connect", "<origin>"]
```

The `<origin>` value MUST be the lowercase, normalized, Punycode-encoded form defined in **Origin Format** below. Tags are signed as part of the NIP-5A manifest and participate in `aggregateHash` via a synthetic xTag entry defined in **Canonical `connect:origins` aggregateHash Fold** below. One origin, one tag; an origin list of length N produces exactly N `connect` tags in the manifest.

Worked example: a manifest fragment declaring three origins (two ASCII, one IDN converted to Punycode):

```
["connect", "https://api.example.com"]
["connect", "wss://events.example.com"]
["connect", "https://xn--caf-dma.example.com"]
```

The napplet-build tool is the sole author of these tags. Napplet authors do NOT hand-edit the manifest; they supply origins to the build tool's `connect: string[]` option and the build tool performs normalization, validation, and emission.

### Origin Format

Each origin MUST conform to the following rules. The napplet-build tool MUST reject any origin that fails these rules at build time; the shell MUST reject any `connect` tag whose value fails these rules at manifest-load time. Both sides enforce because the build rejection catches author errors early and the shell rejection catches tampering after signing.

-   **Scheme whitelist.** The scheme part MUST be one of `https:`, `wss:`, `http:`, or `ws:`. Any other scheme — including `file:`, `ftp:`, `gopher:`, `data:`, `blob:`, and custom schemes — MUST be rejected. This is a closed set; shells MUST NOT accept additional schemes under any policy, and the build tool MUST NOT emit them.
-   **Lowercase scheme and host.** The scheme and host parts MUST be lowercase on the wire. `HTTPS://FOO.COM` is invalid as emitted. The build tool MUST lowercase before emission; the shell MUST reject non-lowercase input with a diagnostic. Shells MUST NOT silently lowercase on lookup — doing so would cause two cosmetically different origins to produce two different aggregateHashes while matching the same CSP directive, breaking the determinism rationale for the fold below.
-   **Punycode IDN.** A host containing any non-ASCII character MUST be Punycode-encoded (`xn--` form, lowercase) before emission. The build tool is the sole authority for the UTF-8 → Punycode conversion. The shell MUST NOT auto-convert and MUST reject any non-Punycode non-ASCII host with a diagnostic. This direction of authority ensures that whatever Punycode form the napplet author signs into the manifest is the byte-identical form the shell sees on lookup.
-   **No wildcards.** The host MUST NOT contain `*`. Wildcard subdomains (for example `https://*.example.com`) MUST be rejected at build time and at manifest-load time. Each subdomain is a separate origin that requires its own `connect` tag and participates independently in the consent prompt.
-   **Default ports banned.** If the port component would be the scheme's default (443 for `https:` and `wss:`, 80 for `http:` and `ws:`), the port MUST be omitted from the origin. `https://foo.com:443` MUST be rejected at build time; `https://foo.com` is the correct form. The rationale is aggregateHash determinism: two cosmetically different forms that CSP treats as equivalent origin-matches would otherwise produce different hashes, silently invalidating prior grants on no semantic change. Shells MUST NOT normalize default ports on lookup for the same reason.
-   **No path, query, or fragment.** The origin is scheme + host + optional non-default port. `https://foo.com/api`, `https://foo.com?x=1`, and `https://foo.com#section` are all invalid. CSP `connect-src` matches at the origin granularity; path-level constraints are out of scope for NAP-CONNECT.
-   **No trailing slash.** The origin MUST NOT end in `/`. `https://foo.com/` is invalid; `https://foo.com` is the correct form. This rule is a consequence of the no-path rule but is worth calling out because many URL-parsing libraries auto-append a trailing slash when stringifying a parsed origin.

#### Origin Normalization Conformance Vectors

The table below enumerates canonical input/output pairs a conformant build tool and a conformant shell MUST agree on. Rows marked `reject` indicate inputs that MUST be refused with a diagnostic; rows with a normalized output indicate inputs that are accepted after producing the specified byte-identical string.

| Input | Normalized | Reason |
| --- | --- | --- |
| `HTTPS://api.example.com` | `https://api.example.com` | Uppercase scheme lowercased by build tool |
| `https://API.example.com` | `https://api.example.com` | Uppercase host lowercased by build tool |
| `https://foo.com/` | *reject* | Trailing slash forbidden |
| `https://foo.com:443` | *reject* | Default port `:443` for `https:` MUST NOT be written |
| `wss://events.example.com:443` | *reject* | Default port `:443` for `wss:` MUST NOT be written |
| `http://foo.com:80` | *reject* | Default port `:80` for `http:` MUST NOT be written |
| `https://api.example.com:8443` | `https://api.example.com:8443` | Non-default port accepted verbatim |
| `https://*.example.com` | *reject* | Wildcard host forbidden |
| `https://foo.com/api` | *reject* | Path not permitted in origin |
| `https://foo.com?x=1` | *reject* | Query not permitted in origin |
| `https://foo.com#top` | *reject* | Fragment not permitted in origin |
| `ftp://files.example.com` | *reject* | Scheme not in whitelist |
| `data:text/plain,hello` | *reject* | Scheme not in whitelist |
| `https://café.example.com` | `https://xn--caf-dma.example.com` | UTF-8 IDN converted to Punycode by build tool |
| `https://xn--caf-dma.example.com` | `https://xn--caf-dma.example.com` | Already Punycode, accepted verbatim |
| `https://CAFÉ.example.com` | *reject* | Build tool rejects uppercase non-ASCII; author MUST supply lowercase or ASCII |
| `https://日本.example.com` | `https://xn--wgv71a.example.com` | Multi-byte UTF-8 IDN converted to Punycode by build tool |
| `https://api.example.com` | `https://api.example.com` | Already canonical, accepted verbatim (VALID accept case) |

Shell implementers MUST produce byte-identical output to the build tool for every accepted row. A disagreement on any row is a conformance bug.

### Canonical `connect:origins` aggregateHash Fold

The set of accepted origins MUST be folded into `aggregateHash` via a synthetic xTag entry so that any change to the origin set invalidates prior grants keyed on `(dTag, aggregateHash)`. The fold is defined as normative pseudocode; the output is a 64-character lowercase hexadecimal SHA-256 digest.

```
# Input:  origins = list[str]
#         Each origin has already been validated and normalized per the
#         Origin Format rules above (scheme lowercased, host lowercased,
#         Punycode-encoded, no default port, no path / query / fragment,
#         no trailing slash).
# Output: a 64-character lowercase hexadecimal SHA-256 digest.

def connect_origins_fold(origins: list[str]) -> str:
    normalized = [o.lower() for o in origins]   # 1. lowercase (idempotent after normalization)
    normalized.sort()                            # 2. ASCII-ascending sort
    joined = "\n".join(normalized)               # 3. join with single LF, NO trailing newline
    encoded = joined.encode("utf-8")             # 4. UTF-8 encode
    digest = sha256(encoded).hexdigest()         # 5. SHA-256
    return digest.lower()                        # 6. lowercase hex (already lowercase from hexdigest)
```

The resulting digest is pushed as a synthetic xTag entry `[<hex-digest>, 'connect:origins']` into the manifest's xTag array **before** `aggregateHash` is computed. This synthetic entry participates in `aggregateHash` but is filtered out of the `['x', ...]` tag projection in the signed manifest — the public manifest surfaces origins as their own `['connect', ...]` tags, not as pseudo-file x-tag entries.

Shells performing grant lookups MUST recompute the fold from the manifest's `['connect', ...]` tags (after normalization-equivalence verification) and MUST verify that the recomputed digest matches the synthetic `connect:origins` xTag's hash. A mismatch MUST result in manifest rejection with a diagnostic; a mismatch indicates either a build-tool bug or post-signing tampering, and in neither case is it safe to proceed. This double-entry — origin tags plus folded hash — is the guard against silent origin-list tampering.

#### Conformance Fixture

The following fixture is normative for interop. A conformant build tool and a conformant shell MUST both produce the specified hexadecimal digest from the specified input origins.

```
Input origins (post-normalization, as supplied by the napplet author):
  https://api.example.com
  https://xn--caf-dma.example.com
  wss://events.example.com

Sorted (ASCII-ascending):
  https://api.example.com
  https://xn--caf-dma.example.com
  wss://events.example.com

Joined with literal LF (\n), no trailing newline:
  https://api.example.com\nhttps://xn--caf-dma.example.com\nwss://events.example.com

UTF-8 byte length of the joined string: 80

Expected SHA-256 lowercase hex digest:
  cc7c1b1903fb23ecb909d2427e1dccd7d398a5c63dd65160edb0bb8b231aa742
```

The digest above can be verified independently with a one-liner such as `python3 -c "import hashlib; print(hashlib.sha256(b'https://api.example.com\nhttps://xn--caf-dma.example.com\nwss://events.example.com').hexdigest())"`. Implementations whose output differs from `cc7c1b1903fb23ecb909d2427e1dccd7d398a5c63dd65160edb0bb8b231aa742` for the specified inputs are non-conformant.

### Runtime API

The napplet-side runtime surface is a single property `connect` mounted at `window.napplet.connect`:

`NappletConnect`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `granted` | `bool` | yes | True when all declared origins are granted for this `(dTag, aggregateHash)`. |
| `origins` | list of `tstr` | yes | User-approved origins; empty on denial or unsupported shells. |

The shim populates `window.napplet.connect` synchronously at install time from a shell-injected `<meta name="napplet-connect-granted" content="...">` element in the served HTML. The meta tag's `content` attribute is a space-separated list of approved origins. An empty `content` value or an absent meta tag indicates `{ granted: false, origins: [] }`.

Shells implementing this NAP MUST inject the discovery meta tag when serving the napplet's HTML — even on denial, in which case the tag is emitted with an empty `content` attribute. The presence of the meta tag is the signal that the shell implements NAP-CONNECT; absence is indistinguishable from a shell that does not advertise `nap:connect`. Shells MUST NOT rely on postMessage traffic to convey grant state; `window.napplet.connect` is populated before any napplet code runs.

`window.napplet.connect` MUST default to `{ granted: false, origins: [] }` on shells that do not implement `nap:connect`, on shells that implement `nap:connect` but denied the prompt, and on shells that have not yet injected the meta tag at shim install. The property MUST NEVER be `undefined`. Napplets gracefully degrade by checking `shell.supports('nap:connect')` before depending on `window.napplet.connect.granted === true`; a napplet that branches solely on `.granted` without checking `shell.supports(...)` will produce correct (degraded) behavior but will not be able to distinguish "shell does not implement the NAP" from "user denied the prompt".

The `NappletConnect` object and its `origins` array SHOULD be frozen via `Object.freeze` at shim install so that napplet code cannot tamper with the state after install. Freezing is a best-effort integrity signal, not a security control — the napplet runs in the same execution context as the shim and can always replace `window.napplet.connect` wholesale. The shell's CSP header, not the shim object, is the authoritative enforcement.

### Capability Advertisement

Shells advertise NAP-CONNECT support through three capability strings on the NIP-5D `shell.supports()` surface:

-   `shell.supports('nap:connect')` — primary capability. Returns `true` when the shell implements this NAP: honors `connect` manifest tags, injects the discovery meta tag, performs the origin-format validation and the `(dTag, aggregateHash)` grant-persistence model, and emits the runtime CSP described in `NAP-CLASS-2.md`. A napplet that branches on `.granted` SHOULD first check this capability to distinguish "shell denied" from "shell does not implement".
-   `shell.supports('connect:scheme:http')` — secondary, operator policy. Returns `true` if the shell permits cleartext `http:` origins in `connect` tags. Operator policy MAY refuse cleartext entirely — some deployments require HTTPS end-to-end and advertise `false` for this capability.
-   `shell.supports('connect:scheme:ws')` — secondary, parallel to `connect:scheme:http`. Returns `true` if the shell permits cleartext `ws:` origins.

Napplets declaring cleartext (`http:` or `ws:`) origins SHOULD check the corresponding `connect:scheme:*` capability before relying on a grant being obtainable and SHOULD gracefully degrade when the shell refuses cleartext. When a shell refuses to serve a napplet solely on the basis of a cleartext origin, the refuse-to-serve diagnostic MUST identify the offending origin and the operator-policy reason, so that the napplet author or a sophisticated end user can understand why the napplet cannot load.

### Shell Consent Flow

The shell consent flow is owned by the posture defined in `NAP-CLASS-2.md`. NAP-CONNECT specifies only the triggering condition and the semantic content of the prompt; the UX wording, layout, and revocation machinery are posture concerns.

The trigger for the consent flow is: the napplet's manifest contains at least one `['connect', '<origin>']` tag AND no prior approve-or-deny decision exists for the composite `(dTag, aggregateHash)` key. Shells encountering this condition MUST present a consent prompt before the iframe is served; shells MUST NOT silently approve or silently deny.

The prompt MUST capture the following semantic elements. Exact wording is a shell-UX decision, but these MUSTs are load-bearing regardless of copy:

1.  Napplet name as declared in the NIP-5A manifest.
2.  The complete list of requested origins (verbatim as declared in the `connect` tags — shells MUST NOT summarize, truncate, or first-presentation-elide the list).
3.  An explicit statement that approval allows the napplet to **send AND receive** any data with the listed origins — the grant is not read-only and is not shell-mediated.
4.  An explicit statement that the shell cannot see or filter post-grant traffic between the napplet and the listed origins.
5.  Visible marking of any cleartext (`http:` or `ws:`) origins alongside the confidentiality tradeoff: the user is approving traffic that is not encrypted in transit.

Shells MUST NOT use diminutive language ('just', 'only', 'simply') that understates the trust cost of approval. Consent prompts written to minimize user friction at the cost of informed consent are misleading the user. See `NAP-CLASS-2.md` for the complete consent-flow MUSTs — including refuse-to-serve behavior on residual meta-CSP, revocation UX, at-next-load-only effective-revocation timing, and the full set of shell-side responsibilities.

### Grant Persistence

Grants are keyed on the composite `(dTag, aggregateHash)`. The `aggregateHash` input includes the `connect:origins` synthetic xTag entry defined in the **Canonical `connect:origins` aggregateHash Fold** section above. Any change to the napplet's origin set (addition, removal, or reorder after normalization) produces a new `aggregateHash`, which auto-invalidates the prior grant and triggers a fresh consent prompt on next load.

Shells MUST key grants on the exact composite. Keying on `dTag` alone is a security bug: a rebuilt napplet with a changed origin set would inherit the prior approval silently, giving the attacker-in-the-rebuild a free pass to reach newly-added origins the user never approved. This is the silent-supply-chain-upgrade vector, and the composite-key rule is the direct mitigation.

See `NAP-CLASS-2.md`'s Grant Persistence Semantics section for the full posture-level MUSTs — including the diff UX requirement on re-prompt when a prior approved grant existed under the same `dTag` but a different `aggregateHash`, and the extended-key hygiene rule (shells MAY extend the key tuple but the extension MUST be a strict superset of `(dTag, aggregateHash)` to preserve portability across conformant shells).

### Security Considerations

#### Post-grant opacity

Once origins are approved and the runtime CSP is emitted, the shell has zero browser-level hook to inspect or filter subsequent network traffic between the napplet and the approved origins. This is a fundamental tradeoff and cannot be papered over. Consent-prompt language MUST reflect this reality — see **Shell Consent Flow** above. Mechanisms that would enable post-grant traffic inspection — audit logging, per-request quotas, traffic filtering — all require a service worker intercepting the napplet's network layer, and the napplet's `sandbox="allow-scripts"` iframe forbids service-worker registration. NAP-CONNECT does not attempt to provide what the browser's security model does not allow.

#### Mixed-content silent failure

Browsers block `http:` and `ws:` fetches from napplets running in shells served over `https:`, regardless of the CSP header's `connect-src` value. A napplet declaring `http:` origins approved by the user will silently fail to fetch when the shell is served over `https:`, except for the `localhost` / `127.0.0.1` secure-context exceptions. The napplet-build tool SHOULD emit a build-time warning when `http:` or `ws:` origins appear in `connect`; shells SHOULD surface this reality in the consent prompt so the user is not surprised by an apparently-approved grant that produces no traffic in practice. Mixed-content is a browser-level rule enforced below the CSP layer; NAP-CONNECT cannot override it.

#### Sandbox preservation

Approving `connect` origins does NOT relax the iframe sandbox. The napplet remains at an opaque origin under `sandbox="allow-scripts"`. Grants relax only the `connect-src` CSP directive; all other browser protections (Same-Origin Policy against the shell's origin, absence of cookies on napplet-origin requests, no service-worker registration, no shared-storage access, no direct access to the shell's DOM) remain in force. A NAP-CONNECT grant is a network-egress permission scoped to specific origins — it is not a sandbox escape, it is not a capability-expansion beyond network, and it does not interact with NAP-RESOURCE's own enforcement layer.

#### Weaker posture than NAP-RESOURCE

NAP-CONNECT is substantially weaker than NAP-RESOURCE in terms of shell-side visibility and policy enforcement. NAP-RESOURCE proxies all fetches through the shell, which can MIME-sniff response bytes, enforce a private-IP block list at DNS-resolution time, rasterize SVG in a sandboxed worker, cap response size, limit redirect chains, and re-validate per hop. NAP-CONNECT does none of those things — the grant is a full trust vote for the listed origins, and every byte flowing through a NAP-CONNECT grant is outside the shell's view. Napplet authors SHOULD default to NAP-RESOURCE for everything NAP-RESOURCE can express, and reach for NAP-CONNECT only when the use case genuinely requires direct `POST`, WebSocket, SSE, streaming responses, custom headers, or long-lived connections.

### Graceful Degradation

Napplets that might want network access SHOULD branch on four states, in priority order:

1.  `shell.supports('nap:connect') === true` AND `window.napplet.connect.granted === true` — the user approved the manifest's `connect` origins. Use direct `fetch` / `WebSocket` / `EventSource` against `window.napplet.connect.origins`.
2.  `shell.supports('nap:connect') === true` AND `window.napplet.connect.granted === false` — user denied the prompt, the prompt has not yet been presented, or the shell implements the NAP but chose not to advertise grants for this napplet. Fall back to NAP-RESOURCE for anything the resource NAP can express; otherwise degrade the affected feature gracefully. Optionally display a napplet-side affordance prompting the user to re-approve through the shell's revocation UI.
3.  `shell.supports('nap:connect') === false` AND `shell.supports('nap:resource') === true` — shell does not implement NAP-CONNECT at all; use NAP-RESOURCE for read-only byte fetches. POST / WebSocket / SSE features are unavailable in this environment and MUST be degraded gracefully; the napplet MUST NOT pressure the user to switch shells.
4.  Neither `nap:connect` nor `nap:resource` is advertised — the napplet cannot reach the network at all. Display appropriate UX (offline mode, read-only-from-cache, or a clear explanation that the feature requires a shell implementing one of the network NAPs) and avoid silent failure.

States (1) and (2) are the common paths in modern shells. States (3) and (4) are graceful-shutdown paths that napplets targeting restrictive or minimal shells MUST handle without crashing, hanging, or presenting confusing error messages.

### References

-   `NIP-5D` — Parent transport spec: JSON envelope wire format, iframe sandbox model, capability advertisement surface.
-   `NAP-CLASS.md` — Parent class track: defines the `class.assigned` envelope, the `window.napplet.class` runtime surface, and the authoring rules for track members.
-   `NAP-CLASS-1.md` — Default strict-baseline posture. Triggered when a napplet declares no `connect` tags, and also when a user denies the NAP-CLASS-2 consent prompt (denied napplets are served under the NAP-CLASS-1 posture).
-   `NAP-CLASS-2.md` — User-approved explicit-origin posture triggered by presence of `connect` tags. Owns the CSP shape, consent-flow MUSTs, grant-persistence semantics, residual-meta-CSP refuse-to-serve requirement, and revocation UX.
-   `NAP-RESOURCE` — Sibling NAP providing shell-mediated read-only byte fetching. Authors SHOULD default to NAP-RESOURCE for everything it can express and reach for NAP-CONNECT only when direct network access is genuinely required.
-   WHATWG URL — Origin format and Punycode IDN conversion rules.
-   WHATWG Fetch, Mixed Content — Browser-level mixed-content enforcement (HTTPS shells cannot fetch HTTP origins; `localhost` / `127.0.0.1` secure-context exceptions).

### Changelog

-   `fdf6c3e` - Introduced NAP-CONNECT for user-gated direct network access.

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

On this pageSpecification metadata

Capability domain

`connect`

Repository state

Draft PR

Registry status

Deferred

Document marker

draft

Declared dependencies

None declared in the preamble

Source revision

`ec81a3a4a1fc`

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