NAP-SERIAL / PR #61
Shell-Mediated Serial Device Access
Full specification from the fetched upstream revision ↗. The document below is rendered from its original Markdown. Follow the latest source on GitHub ↗.
NAP-SERIAL
Shell-Mediated Serial Device Access
draft
NAP ID: NAP-SERIAL
Domain: serial
Depends: none.
Web binding (NIP-5D): 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
serialwith the Web Serial API. - Native runtimes MAY implement
serialwith 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 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
idfor correlation. serial.eventhas noid; it is runtime-pushed.datais a byte array. Values outside0..255are 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
infofields.
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
labelwhen 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.