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

NAP-SERIAL / PR #61

# Shell-Mediated Serial Device Access

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: experimental**

Serial binding has unit coverage; device and native-host integration evidence is limited.

Implementation proposal: [in-review ↗](https://github.com/napplet/naps/pull/61), 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/0c6e88ab8003f5bf88c8ddee5b5e3cd382ab7e1c/naps/NAP-SERIAL.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/61/files).

## NAP-SERIAL

### Shell-Mediated Serial Device Access

`draft`

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

### Description

NAP-SERIAL lets a sandboxed napplet communicate with user-approved serial devices without receiving direct access to browser serial APIs, native handles, or OS device paths. The runtime owns device selection, permission prompts, port opening, read loops, write ordering, disconnect handling, and policy. The napplet gets a small byte-oriented session API.

This NAP defines the runtime capability, not a specific implementation backend. A web projection can implement it with the Web Serial API. A native projection can implement it with OS serial APIs, Tauri plugins, or another native backend. The napplet-facing contract stays the same.

This NAP does not define a device protocol. Text encoding, packet framing, checksums, and command semantics belong to the napplet or to higher-level protocols built on top of this byte channel.

### API Surface

| Operation | Parameters | Result | Wire |
| --- | --- | --- | --- |
| `open` | `request` (`SerialOpenRequest`) | `SerialOpenResult` | `serial.open` / `serial.open.result` |
| `write` | `sessionId` (`tstr`), `data` (`bstr`) | none | `serial.write` / `serial.write.result` |
| `close` | `sessionId` (`tstr`), optional `reason` (`tstr`) | none | `serial.close` / `serial.close.result` |
| `onEvent` | handler for `SerialEvent` | `Subscription` handle | `serial.event` |

#### Schemas

Enums:

| Name | Values |
| --- | --- |
| `SerialState` | `opening`, `open`, `closed` |
| `SerialDataBits` | `7`, `8` |
| `SerialStopBits` | `1`, `2` |
| `SerialParity` | `none`, `even`, `odd` |
| `SerialFlowControl` | `none`, `hardware` |

`SerialOpenRequest` fields:

| Field | Required | Type |
| --- | --- | --- |
| `filters` | no | list of `SerialPortFilter` |
| `options` | yes | `SerialOpenOptions` |
| `label` | no | text |

`SerialPortFilter` fields:

| Field | Required | Type |
| --- | --- | --- |
| `usbVendorId` | no | unsigned integer |
| `usbProductId` | no | unsigned integer |
| `bluetoothServiceClassId` | no | text or unsigned integer |

`SerialOpenOptions` fields:

| Field | Required | Type |
| --- | --- | --- |
| `baudRate` | yes | unsigned integer |
| `dataBits` | no | `SerialDataBits` |
| `stopBits` | no | `SerialStopBits` |
| `parity` | no | `SerialParity` |
| `bufferSize` | no | unsigned integer |
| `flowControl` | no | `SerialFlowControl` |

`SerialOpenResult` fields:

| Field | Required | Type |
| --- | --- | --- |
| `session` | yes | `SerialSession` |

`SerialSession` fields:

| Field | Required | Type |
| --- | --- | --- |
| `id` | yes | text |
| `state` | yes | `SerialState` |
| `info` | no | `SerialPortInfo` |

`SerialPortInfo` fields:

| Field | Required | Type |
| --- | --- | --- |
| `usbVendorId` | no | unsigned integer |
| `usbProductId` | no | unsigned integer |
| `bluetoothServiceClassId` | no | text or unsigned integer |
| `displayName` | no | text |

`SerialEvent` variants:

| Variant | Required fields | Optional fields |
| --- | --- | --- |
| state | `type: "state"`, `sessionId: tstr`, `state: SerialState` | none |
| data | `type: "data"`, `sessionId: tstr`, `data: bstr` | none |
| closed | `type: "closed"`, `sessionId: tstr` | `reason: tstr` |

**`open(request)`** asks the runtime to select and open a serial port. The runtime may use `filters`, `label`, and napplet policy to explain or narrow the chooser. It returns a runtime-assigned session id after the port is open.

**`write(sessionId, data)`** writes bytes to an open session. The runtime preserves write order per session.

**`close(sessionId, reason)`** closes a session. The runtime may also close sessions when a device disconnects, policy changes, or the napplet unloads.

**`onEvent(handler)`** subscribes to runtime-pushed session state, data, and close events. Device data is delivered as byte values in the range `0..255`.

### Projection Backends

NAP-SERIAL is portable across projections:

-   Web runtimes MAY implement `serial` with the Web Serial API.
-   Native runtimes MAY implement `serial` with OS serial APIs, Tauri plugins, or another native serial backend.
-   Runtimes MUST NOT expose backend-specific handles to napplets.
-   Projection-specific chooser UX, remembered devices, device paths, permission prompts, and stream objects stay runtime-owned.

### Wire Protocol

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

| Type | Direction | Payload fields |
| --- | --- | --- |
| `serial.open` | napplet -> runtime | `id`, `request` |
| `serial.open.result` | runtime -> napplet | `id`, `session?`, `error?` |
| `serial.write` | napplet -> runtime | `id`, `sessionId`, `data` |
| `serial.write.result` | runtime -> napplet | `id`, `error?` |
| `serial.close` | napplet -> runtime | `id`, `sessionId`, `reason?` |
| `serial.close.result` | runtime -> napplet | `id`, `error?` |
| `serial.event` | runtime -> napplet | `event` |

Key design notes:

-   Request/result pairs use `id` for correlation.
-   `serial.event` has no `id`; it is runtime-pushed.
-   `data` is a byte array. Values outside `0..255` are invalid.
-   The runtime never exposes raw `SerialPort`, `ReadableStream`, `WritableStream`, `navigator.serial`, native handles, or OS device paths to the napplet.

#### Examples

**Open a serial session:**

```
-> {
     "type": "serial.open",
     "id": "s1",
     "request": {
       "filters": [{ "usbVendorId": 9025 }],
       "options": { "baudRate": 115200 },
       "label": "controller"
     }
   }
<- {
     "type": "serial.open.result",
     "id": "s1",
     "session": {
       "id": "serial-1",
       "state": "open",
       "info": { "usbVendorId": 9025 }
     }
   }
```

**Receive and write bytes:**

```
<- {
     "type": "serial.event",
     "event": { "type": "data", "sessionId": "serial-1", "data": [79, 75, 10] }
   }
-> {
     "type": "serial.write",
     "id": "w1",
     "sessionId": "serial-1",
     "data": [112, 105, 110, 103, 10]
   }
<- { "type": "serial.write.result", "id": "w1" }
```

**Device closed:**

```
<- {
     "type": "serial.event",
     "event": {
       "type": "closed",
       "sessionId": "serial-1",
       "reason": "device disconnected"
     }
   }
```

#### Error Handling

Result messages include an `error` string when the runtime cannot fulfill the request. Common errors: `"unsupported"`, `"policy denied"`, `"user cancelled"`, `"permission denied"`, `"port unavailable"`, `"open failed"`, `"session not found"`, `"invalid data"`, `"write failed"`, and `"device disconnected"`.

When `serial.open.result` includes `error`, it MUST NOT include `session`.

### Runtime Behavior

-   The runtime MUST own all calls to Web Serial, native serial APIs, or any other serial backend.
-   The runtime MUST respond to every request with a result message carrying the same `id`.
-   The runtime MUST obtain user approval and satisfy backend activation or permission requirements before opening a port.
-   The runtime MUST scope sessions to the requesting napplet and close them when that napplet unloads or loses permission.
-   The runtime MUST preserve write order per session.
-   The runtime MUST deliver readable bytes through `serial.event`.
-   The runtime MUST NOT expose raw serial objects, browser stream objects, native handles, or OS device paths to napplets.
-   The runtime MAY remember user-approved ports, enforce per-napplet allowlists, limit payload sizes, limit concurrent sessions, or redact `info` fields.

### Security Considerations

Serial devices may control hardware, leak local data, or expose identifying device information. The runtime is the policy boundary.

-   Napplets MUST NOT receive raw serial permissions, raw port objects, native handles, or OS device paths.
-   The runtime MUST require explicit user or policy approval before opening a serial session.
-   The runtime SHOULD show which napplet is requesting access and why, using the optional `label` when helpful.
-   The runtime SHOULD treat incoming bytes as untrusted input and SHOULD apply reasonable rate, size, and lifecycle limits.
-   The runtime SHOULD expose only the minimum device metadata needed by the napplet.

### Implementations

-   None yet.

### Changelog

-   `85f6ddb` - Introduced NAP-SERIAL as a portable serial access surface.
    
-   `a26e4de` - Adopted injected-domain availability and linked the current upstream web binding.
    

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

On this pageSpecification metadata

Capability domain

`serial`

Repository state

Open PR

Registry status

Not listed in the registry

Document marker

draft

Declared dependencies

None declared in the preamble

Source revision

`0c6e88ab8003`

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