NAP-CAPTURE / PR #94
Shell-Mediated Microphone Recording
Full specification from the fetched upstream revision ↗. The document below is rendered from its original Markdown. Follow the latest source on GitHub ↗.
NAP-CAPTURE
Shell-Mediated Microphone Recording
draft
NAP ID: NAP-CAPTURE
Domain: capture
Depends: none.
Web binding (NIP-5D): window.napplet.capture; domain presence signals availability.
Description
NAP-CAPTURE lets a napplet ask its runtime to create a finite, user-approved microphone recording. The runtime owns consent, device selection, platform permission, capture, encoding, limits, indication, retention, and teardown.
The napplet receives encoded audio bytes, not microphone authority. It never receives a device handle, stable device identifier, raw stream, recorder object, or platform permission state.
NAP-CAPTURE has no dependency on another NAP. Passing a completed artifact to
upload, storage, relay, or another domain is a separate napplet action.
Discovery
In the web projection, a napplet detects the capability by checking whether
window.napplet.capture is present. No shell handshake is required.
A runtime that reports support MUST implement every baseline operation in this document.
info() reports output formats and finite policy limits. It MUST NOT reveal
stable device identifiers, device labels, device counts, or platform permission
state.
API Surface
| Operation | Parameters | Result | Wire |
|---|---|---|---|
info |
— | CaptureInfo |
capture.info → capture.info.result |
start |
CaptureRequest |
CaptureSession |
capture.start → capture.start.result |
status |
captureId |
CaptureStatus |
capture.status → capture.status.result |
stop |
captureId |
CaptureArtifact |
capture.stop → capture.stop.result |
cancel |
captureId |
CaptureStatus |
capture.cancel → capture.cancel.result |
release |
captureId |
captureId |
capture.release → capture.release.result |
status is authoritative. capture.changed is advisory; a missed event never
loses a terminal artifact or terminal error before its disclosed expiry.
CaptureInfo
| Field | Required | Type | Notes |
|---|---|---|---|
sources |
yes | list of text | MUST contain only "microphone" in this version. |
mimeTypes |
yes | list of text | MIME types the runtime is prepared to attempt, in preference order. |
maxDurationMs |
yes | positive integer | Maximum interval before the runtime initiates terminalization. |
maxBytes |
yes | positive integer | Hard maximum deliverable artifact size. |
retentionMs |
yes | positive integer | Time a terminal status remains recoverable unless explicitly erased. |
CaptureInfo is advisory. Policy, device availability, encoder startup, and
platform permission can still make a later start fail. All three limits MUST
be finite; a runtime MUST NOT advertise null or an unbounded value.
CaptureRequest
| Field | Required | Type | Notes |
|---|---|---|---|
source |
yes | text | MUST be "microphone". |
mimeTypes |
no | list of text | Acceptable output MIME types in caller preference order. |
An omitted or empty mimeTypes list lets the runtime choose any advertised
format. Each supplied item MUST be a valid MIME media type or start fails with
invalid-request before consent UI. MIME compatibility is evaluated by parsing
media types. Type, subtype, and parameter names are ASCII case-insensitive.
Duplicate parameter names make a request item invalid. Parsing removes optional
whitespace outside a parameter value. Quoted-string escaping is removed, but
whitespace inside the quoted value is preserved. Parameter values are otherwise
compared byte-for-byte and case-sensitively. For the codecs parameter, split the
unquoted comma-separated value and remove optional whitespace around each token.
Compare the resulting ordered token list case-sensitively by default. A
registered codec namespace MAY define a different comparison rule; apply it only
when both tokens belong to that namespace. Every
parameter requested by the caller MUST be present and equal under this algorithm
in the actual output; additional actual parameters are allowed. Parameter order
does not affect compatibility. The runtime MUST apply this same algorithm
wherever this document says MIME-compatible.
The runtime MUST choose an advertised format compatible with at least one
requested item or fail with unsupported-format before consent UI. The returned
mimeType is the normalized actual output type, not necessarily the caller's
original string. An advertised format is one the runtime is prepared to attempt,
not a guarantee that the platform encoder will later start successfully.
The request has no device selector or low-level audio constraints. A runtime MAY offer device and quality choices in trusted shell UI.
CaptureSession
| Field | Required | Type | Notes |
|---|---|---|---|
captureId |
yes | text | Opaque identifier bound to the authenticated requesting endpoint generation. |
source |
yes | text | "microphone". |
mimeType |
yes | text | Actual output MIME type selected by the runtime. |
maxDurationMs |
yes | positive integer | Effective terminalization deadline interval for this session. |
maxBytes |
yes | positive integer | Effective hard artifact-size limit for this session. |
retentionMs |
yes | positive integer | Effective terminal-status retention interval for this session. |
A successful start result means consent succeeded and recording is active. A
runtime MUST NOT return a CaptureSession while consent is pending.
CaptureArtifact
| Field | Required | Type | Notes |
|---|---|---|---|
captureId |
yes | text | Identifier of the completed capture. |
data |
yes | any | Projection-defined carrier for one immutable, complete encoded audio byte sequence. It is never text, base64, a live stream, or a device/recorder handle. |
mimeType |
yes | text | Normalized actual MIME type of data. |
size |
yes | non-negative integer | Exact byte length of data. |
durationMs |
yes | non-negative integer | Runtime-measured active capture interval. |
truncated |
yes | boolean | true when a limit or interruption ended capture. |
reason |
yes | "requested", "user", "limit", or "interrupted" |
Terminal cause. |
durationMs is monotonic elapsed time from recorder activation to the terminal
decision, excluding consent and backend finalization time; it is not parsed
container duration. maxDurationMs requires the runtime to initiate the terminal
decision by that deadline, but scheduler and backend latency MAY make reported
durationMs larger. maxBytes is a hard upper bound on a deliverable artifact.
size MUST equal the binary byte length and MUST NOT exceed the session's
maxBytes. The runtime MUST report the actual container and codec
in mimeType; it MUST NOT merely echo the request.
CaptureStatus
| Field | Required | Type | Notes |
|---|---|---|---|
captureId |
yes | text | Affected capture. |
state |
yes | "recording", "completed", "cancelled", or "failed" |
Current observable state. |
artifact |
completed only | CaptureArtifact |
Retained terminal artifact. |
reason |
cancelled only | "requested", "user", or "revoked" |
Stable cancellation reason. |
error |
failed only | CaptureError |
Retained normalized failure. |
expiresAt |
terminal only | non-negative integer | Unix time in milliseconds when retained terminal state is automatically erased. |
A recording status MUST omit artifact, reason, error, and expiresAt.
A completed status MUST contain only artifact and expiresAt among those
conditional fields; cancelled MUST contain only reason and expiresAt; and
failed MUST contain only error and expiresAt.
A terminal CaptureStatus remains available until release, trusted discard,
napplet unload, identity revocation, or expiresAt, whichever comes first. The
runtime MUST set expiresAt to the terminal commit time plus the session's
retentionMs and automatically erase retained runtime state at that time. A new
start for the identity fails with busy while a pending, recording, finalizing,
or retained capture occupies its slot.
CaptureEvent
| Field | Required | Type | Notes |
|---|---|---|---|
captureId |
yes | text | Affected capture. |
state |
yes | "completed", "cancelled", or "failed" |
New terminal state. |
reason |
cancelled only | "requested", "user", or "revoked" |
Stable cancellation reason. |
error |
failed only | CaptureError |
Normalized failure. |
capture.changed carries CaptureEvent. It is a wake-up hint only. The napplet
uses status to retrieve the authoritative terminal state and artifact.
There is no subscribe or unsubscribe operation. The runtime automatically pushes
capture.changed to the bound napplet endpoint while that endpoint generation
remains present and authorized. A projection MAY expose an idiomatic
event-listener shape as guidance, but that shape is not part of this NAP contract.
CaptureError
| Field | Required | Type | Notes |
|---|---|---|---|
code |
yes | text | Stable error code from the table below. |
message |
no | text | Safe diagnostic text; MUST NOT expose device identifiers or platform internals. |
Lifecycle
An endpoint generation is one authenticated lifetime of a projection endpoint. Each projection MUST define when a generation begins and ends.
Each runtime MUST reserve at most one pending, recording, finalizing, or retained
capture slot per verified napplet identity. The slot's owner is the authenticated
endpoint generation that issued start; identity is the scope for attribution,
rate limits, and concurrency policy, not authorization to operate another
endpoint's capture. The runtime MUST also enforce finite runtime-wide limits on
active captures and retained encoded bytes. The identity slot is reserved before
trusted consent UI opens. A concurrent start for that identity fails with
busy. Denial, cancellation, or start failure releases a pending reservation.
start approved
absent --------------------> recording
|
stop/runtime | cancel
decide/finalize| discard
v
completed / cancelled / failed
|
release / expiry / teardown
v
absent
Terminal decisions
A terminal decision selects the intended outcome before asynchronous recorder
finalization. The first accepted caller or runtime terminal decision wins, except
that trusted Discard, permission or policy revocation, identity revocation,
and owner-endpoint unload MUST supersede an uncommitted completion decision and
erase runtime-held bytes. A runtime MAY use an internal finalizing state. While
completion is finalizing, status reports recording; a second stop, cancel,
or release fails with invalid-state. No artifact or terminal event exists
until terminal commit.
Terminal commit atomically stores the terminal CaptureStatus, makes any
completed artifact eligible for delivery, sets expiresAt, frees active device
resources, and emits the one advisory event. Backend callbacks after commit MUST
NOT alter the status, create another artifact, or deliver more bytes.
Rules:
startvalidates the request and reserves policy capacity before opening trusted UI. It creates a session only after consent succeeds, microphone input is active, and the encoder has started. A pre-session rejection creates nocaptureId, status, or event.statusreturns the observable state. Forcompleted, it returns the retained artifact. Forcancelledorfailed, it returns the retained reason or error.stoponrecordingclaims a completion decision and remains pending until finalization commits or fails. On success it retains and returns one artifact withreason: "requested".stoponcompletedreturns the same retained artifact.stoponcancelledorfailedfails withinvalid-state.cancelonrecordingclaims a destructive decision, stops input, erases runtime-held bytes, commitscancelledwithreason: "requested", and returns that status.cancelon a terminal capture returns the retained status without altering or erasing it.releaseis valid only for a committed terminal capture. It erases retained runtime status and bytes. Later operations, including a repeatedrelease, fail withcapture-not-found.- At
expiresAt, the runtime performs the same erasure asreleasewithout a request or event. The identity slot then becomes available. - A duration limit requires the runtime to initiate terminalization no later
than
maxDurationMsafter activation. A byte limit requires the runtime to stop before or when it determines the next complete valid artifact may exceed policy. If a complete valid artifact is available withinmaxBytes, commitcompletedwithtruncated: trueandreason: "limit". The runtime MUST NOT byte-truncate an encoded container. If a complete valid artifact cannot fit, erase it and commitfailedwithquota-exceeded. - Device loss MAY commit a complete recoverable artifact within
maxBytesascompleted,truncated: true,reason: "interrupted"while recording authority remains valid. Otherwise it commitsfailedwithdevice-unavailableand erases bytes. Device loss MUST NOT leave recording active. - Permission, policy, or identity revocation stops input and prevents any
uncommitted artifact delivery. Permission or policy revocation while the owner
endpoint remains valid commits
cancelledwithreason: "revoked"; identity revocation erases all matching runtime state without further delivery. - Owner-endpoint unload cancels pending consent, stops input, and erases that endpoint generation's runtime state without a result or event. It MUST NOT erase a capture merely because another live endpoint of the same identity unloads, and it MUST NOT continue recording in the background.
Trusted user controls
| Trusted action | Required outcome |
|---|---|
| Decline or dismiss consent before activation | capture.start.result with user-cancelled; no captureId, status, artifact, or event. |
| trusted Stop during recording | Complete and retain one artifact with reason: "user". |
| trusted Discard during recording or finalization | Stop input, erase runtime-held bytes, and commit cancelled with reason: "user". |
| trusted Discard after completion | Erase retained runtime state immediately; later operations return capture-not-found. |
Every terminal commit to completed, cancelled, or failed emits exactly one
capture.changed event to the owning endpoint generation while it remains
present and authorized. Event loss is harmless before expiry because status is
authoritative.
A napplet cannot programmatically cancel consent before start returns because
no captureId exists yet. The runtime MUST cancel pending consent when the owner
endpoint unloads; trusted shell UI MUST let the user cancel it directly.
Pause/resume is not a baseline v1 operation.
Wire Protocol
All request messages carry a caller-generated correlation id. While the source
endpoint generation remains present and authorized, each processed request MUST
settle exactly once with its matching result type, which echoes that id.
Endpoint unload or revocation cancels undeliverable pending responses.
Request-ID replay behavior is owned by the common transport, not this domain, and
callers MUST use a fresh id for each operation while an earlier request is
pending.
The projection authenticates both the source endpoint generation and its verified
manifest and artifact identity before dispatch. A captureId is generated by
the runtime and authorized only for the authenticated endpoint generation that
created it. The verified identity remains attribution and policy scope. A request
from a foreign identity, a stale/reloaded endpoint generation, or another live
endpoint of the same identity MUST fail uniformly with capture-not-found.
Responses and events MUST be delivered only to the owning endpoint generation.
| Type | Direction | Payload fields |
|---|---|---|
capture.info |
napplet → runtime | id |
capture.info.result |
runtime → napplet | id, exactly one of info or error |
capture.start |
napplet → runtime | id, request |
capture.start.result |
runtime → napplet | id, exactly one of session or error |
capture.status |
napplet → runtime | id, captureId |
capture.status.result |
runtime → napplet | id, exactly one of status or error |
capture.stop |
napplet → runtime | id, captureId |
capture.stop.result |
runtime → napplet | id, exactly one of artifact or error |
capture.cancel |
napplet → runtime | id, captureId |
capture.cancel.result |
runtime → napplet | id, exactly one of status or error |
capture.release |
napplet → runtime | id, captureId |
capture.release.result |
runtime → napplet | id, exactly one of captureId or error |
capture.changed |
runtime → napplet | event |
Result and error routing
| Situation | Correlated request outcome | Retained status / event |
|---|---|---|
| Malformed or unsupported request, policy denial, consent decline, or failure before a session exists | Matching *.result with error |
None |
Unknown, foreign, stale, or wrong-state captureId |
Matching *.result with error |
No transition and no event |
| Successful operation | One matching *.result |
If and only if it causes the initial terminal commit, retain status and emit one event |
Failure while a requested stop is finalizing |
capture.stop.result with the terminal error |
Commit failed and emit one event |
Pending stop superseded by trusted Discard |
capture.stop.result with user-cancelled |
Commit cancelled with reason: "user" and emit one event |
Pending stop superseded by platform-permission or shell-policy revocation |
capture.stop.result with permission-denied or policy-denied, respectively |
Commit cancelled with reason: "revoked" and emit one event |
An asynchronous terminal failure after successful start, with no operation awaiting completion |
No request response | Commit failed and emit one event; never emit an uncorrelated result |
A pre-session failure and an asynchronous terminal failure are distinct. A
start remains pending until trusted consent, platform permission, and encoder
startup settle. If it fails, only its correlated error is sent. Once a
CaptureSession exists, a later recorder or device failure is authoritative
through retained status and capture.changed.
Examples
Discover formats and limits:
{ "type": "capture.info", "id": "cap-info-1" }
{
"type": "capture.info.result",
"id": "cap-info-1",
"info": {
"sources": ["microphone"],
"mimeTypes": ["audio/webm;codecs=opus", "audio/mp4"],
"maxDurationMs": 600000,
"maxBytes": 52428800,
"retentionMs": 300000
}
}
Start recording:
{
"type": "capture.start",
"id": "cap-start-1",
"request": {
"source": "microphone",
"mimeTypes": ["audio/webm;codecs=opus", "audio/mp4"]
}
}
{
"type": "capture.start.result",
"id": "cap-start-1",
"session": {
"captureId": "capture_7f2a",
"source": "microphone",
"mimeType": "audio/webm;codecs=opus",
"maxDurationMs": 600000,
"maxBytes": 52428800,
"retentionMs": 300000
}
}
Stop and retain the artifact:
{
"type": "capture.stop",
"id": "cap-stop-1",
"captureId": "capture_7f2a"
}
{
"type": "capture.stop.result",
"id": "cap-stop-1",
"artifact": {
"captureId": "capture_7f2a",
"data": <binary audio/webm 18234 bytes>,
"mimeType": "audio/webm;codecs=opus",
"size": 18234,
"durationMs": 2410,
"truncated": false,
"reason": "requested"
}
}
Runtime-driven completion emits a hint:
{
"type": "capture.changed",
"event": {
"captureId": "capture_7f2a",
"state": "completed"
}
}
The napplet then calls capture.status; the completed result contains the
retained artifact.
Runtime Behavior
A conforming runtime:
- MUST own microphone access, device selection, encoding, retention, and teardown.
- MUST authenticate the source endpoint generation, bind each session and
captureIdto it, and separately retain the verified napplet identity for attribution and policy. - MUST obtain a fresh capture-specific confirmation immediately before every activation. Trusted UI MUST identify the verified requesting napplet using a shell-controlled human label, make its publisher, manifest reference, and verified artifact hash available, name the microphone source, and state that complete encoded audio will be delivered to that napplet and may be copied or sent elsewhere by it. A manifest declaration, prior approval, platform permission grant, or unrelated user gesture MUST NOT substitute for this confirmation.
- MUST render trusted shell-controlled indication while microphone capture is active, including the requesting napplet identity and distinct shell-level Stop and Discard controls with the outcomes defined above.
- MUST keep the indicator active while the platform microphone remains active.
- MUST normalize platform errors and keep low-level device diagnostics local.
- MUST enforce finite duration, artifact-byte, retained-byte, prompt-rate, and concurrency policy independently of caller input.
- MUST key prompt-rate policy at least to verified napplet identity so endpoint
reload does not reset it. The policy MUST define a finite prompt budget within
a positive time window and MUST be checked before opening or refreshing trusted UI;
over-budget calls fail with
policy-deniedwithout showing another capture prompt. - MUST stop capture promptly when the user, platform, shell policy, or owning endpoint lifecycle revokes authority.
- MUST erase runtime-held cancelled, released, expired, failed, and unloaded capture bytes and prevent future delivery from those states.
- MUST NOT upload, publish, sign, relay, transcribe, or persist beyond this lifecycle as a side effect of capture.
The runtime MAY provide trusted device selection, quality controls, or previews in shell UI. Those controls do not alter this contract.
Errors
| Code | Meaning |
|---|---|
invalid-request |
The envelope, required field, value type, or MIME syntax is malformed. |
policy-denied |
Runtime policy rejected the operation, including prompt/capacity denial or revocation. |
user-cancelled |
Trusted UI declined consent or superseded a pending operation with Discard. |
permission-denied |
Platform microphone permission was denied or revoked. |
device-unavailable |
No usable microphone exists, or an active device was lost without a recoverable artifact. |
unsupported-source |
source is not "microphone". |
unsupported-format |
No advertised output is compatible with an acceptable requested MIME type. |
busy |
A pending, recording, finalizing, or retained capture reserves the identity slot, or a runtime-wide limit was reached. |
capture-not-found |
The identifier is unknown, expired, stale, or not owned by the authenticated endpoint generation. |
invalid-state |
The requested operation is invalid for the observable state or an internal finalization is pending. |
quota-exceeded |
No complete valid artifact can be retained within byte policy. |
capture-failed |
Capture or encoding failed for another normalized reason. |
A runtime MUST avoid exposing whether a foreign captureId exists; use
capture-not-found for unknown, expired, stale, and foreign identifiers. Request
validation MUST occur before consent UI. Failures after a session exists follow
the result-and-error routing table and MUST NOT leave input active.
Security Considerations
Microphone access can expose private speech and ambient audio. This contract assumes a conforming trusted runtime: a malicious runtime already holds platform microphone authority and cannot be constrained by napplet messages. Within that trust model, the runtime is the security boundary and MUST mediate capture as a visible, revocable capability.
- Platform permission is not napplet consent. A prior platform grant MUST NOT authorize silent capture by a newly loaded napplet.
- Consent UI and active-capture indication MUST be rendered by trusted shell UI, not supplied solely by the napplet.
- Device identifiers, labels, counts, and permission state MUST NOT cross this API.
- Capture identifiers MUST be unguessable and bound to the authenticated owner endpoint generation. Identity equality alone MUST NOT authorize a sibling instance, stale frame, or replacement endpoint.
- Runtime limits MUST prevent unbounded recording, retained-byte growth, repeated prompt abuse, and concurrent microphone monopolization.
- Completed bytes are sensitive. They MUST be delivered only to the owning
endpoint generation.
release, trusted discard, expiry, unload, and revocation erase runtime-held copies and prevent future runtime delivery; they cannot revoke or erase bytes already delivered to the napplet, copied by it, or sent elsewhere. - Live audio, waveform, and level streaming are excluded. They add continuous listening and high-frequency transport semantics not needed here.
Projection
Each projection MUST define its endpoint-generation lifecycle and the normative
carrier and byte-length operation for CaptureArtifact.data. The web binding is
defined by the web projection.
Non-Goals
NAP-CAPTURE v1 does not define:
- camera, screen, window, tab, system-audio, or mixed-source capture;
- device enumeration, device selection APIs, or persistent device preferences;
- raw audio streams, chunks, waveform data, level meters, or WebRTC tracks;
- pause/resume;
- editing, transcoding, transcription, upload, signing, publishing, or relay use;
- durable recovery after napplet unload;
- background recording after napplet unload; or
- any change to a projection's sandbox policy.
Implementations
None yet. A runtime prototype demonstrating consent, trusted indication, binary delivery, terminal retention, limit handling, and unload cleanup is required before this draft should be considered mergeable.
Changelog
8f69d1e- Define runtime-owned microphone consent, capture lifecycle, retained artifacts, wire messages, limits, and security rules.3b83f79- Define automatic change delivery and a language-neutral release result.88b8b49- Bind captures to endpoint generations and make consent, limits, retention, terminal races, binary delivery, and errors deterministic.ad1c293- Move web carriers to the projection, preserve codec identifier case, use matching result envelopes, and avoid introducing a canonical byte type.bddbafd- Adopted injected-domain discovery and verified manifest/artifact attribution while preserving endpoint-generation ownership.