NAP-NOTIFY / PR #11
Shell-Rendered Notifications
Full specification from the fetched upstream revision ↗. The document below is rendered from its original Markdown. Follow the latest source on GitHub ↗.
NAP-NOTIFY
Shell-Rendered Notifications
draft
NAP ID: NAP-NOTIFY
Domain: notify
Depends: none.
Web binding (NIP-5D): window.napplet.notify; domain presence signals availability.
Description
NAP-NOTIFY provides notification delivery between napplets and the shell. Napplets that need to alert users -- incoming messages, completed tasks, background events -- send notification requests to the shell. The shell renders notifications using its own UI (toasts, system notifications, badge counts) and routes user interaction back to the napplet.
Napplets do not render notifications themselves. The shell controls presentation, grouping, priority, and dismissal. This ensures consistent UX across napplets and allows the shell to enforce notification policies (rate limiting, do-not-disturb, channel muting).
API Surface
| Operation | Parameters | Result | Wire |
|---|---|---|---|
send |
notification (NotificationPayload) |
NotificationResult |
notify.send / notify.send.result |
dismiss |
notificationId (tstr) |
none | notify.dismiss |
badge |
count (uint) |
none | notify.badge |
registerChannel |
channel (NotificationChannel) |
none | notify.registerChannel |
requestPermission |
optional channel (tstr) |
PermissionResult |
notify.requestPermission / notify.requestPermission.result |
onAction |
handler for notificationId and actionId |
Subscription handle |
notify.action |
onClicked |
handler for notificationId |
Subscription handle |
notify.clicked |
onDismissed |
handler for notificationId and optional reason |
Subscription handle |
notify.dismissed |
onControls |
handler for list of NotifyControl |
Subscription handle |
notify.controls |
Schemas
Enumerations:
| Name | Values |
|---|---|
NotificationPriority |
low, normal, high, urgent |
NotifyControl |
toasts, badges, actions, channels, system |
NotificationPayload:
| Field | Type | Required | Description |
|---|---|---|---|
title |
tstr |
yes | Notification title. |
body |
tstr |
no | Notification body text. |
icon |
tstr |
no | Icon URL or shell-understood icon reference. |
actions |
list of NotificationAction |
no | Action buttons. |
channel |
tstr |
no | Notification channel ID. |
priority |
NotificationPriority |
no | Notification urgency hint. |
NotificationResult:
| Field | Type | Required | Description |
|---|---|---|---|
notificationId |
tstr |
yes | Shell-assigned notification ID. |
error |
tstr |
no | Error reason when notification delivery failed. |
NotificationAction:
| Field | Type | Required | Description |
|---|---|---|---|
id |
tstr |
yes | Action identifier returned by notify.action. |
label |
tstr |
yes | User-visible action label. |
NotificationChannel:
| Field | Type | Required | Description |
|---|---|---|---|
channelId |
tstr |
yes | Channel identifier. |
label |
tstr |
yes | User-visible channel label. |
description |
tstr |
no | User-visible channel description. |
defaultPriority |
NotificationPriority |
no | Default priority for this channel. |
PermissionResult:
| Field | Type | Required | Description |
|---|---|---|---|
granted |
bool |
yes | Whether notification permission was granted. |
send(notification) -- Send a notification to the shell. The shell decides how to render it (toast, system notification, etc.) based on priority, channel, and shell policy. Returns the assigned notificationId. The shell MAY reject the notification (e.g., permission denied, rate limited).
dismiss(notificationId) -- Request dismissal of a notification by ID. Fire-and-forget. The shell may have already dismissed it.
badge(count) -- Set the badge count for this napplet. The shell displays the badge on the napplet's tile, tab, or icon. Pass 0 to clear. Fire-and-forget.
registerChannel(channel) -- Register a notification channel. Channels let users control notification categories independently (e.g., mute "promotions" but keep "messages"). Fire-and-forget. The shell MAY ignore channels it does not support.
requestPermission(channel?) -- Request permission to send notifications. If channel is provided, requests permission for that specific channel. Returns whether permission was granted. The shell MAY prompt the user or apply policy silently.
onAction(callback) -- Listen for action button clicks. When a user clicks an action button on a notification, the shell sends the notificationId and actionId. Returns a Subscription with close().
onClicked(callback) -- Listen for notification body clicks. Returns a Subscription with close().
onDismissed(callback) -- Listen for notification dismissals. The reason field indicates why: 'user' (user dismissed), 'timeout' (auto-dismissed), 'replaced' (replaced by another notification). Returns a Subscription with close().
onControls(callback) -- Listen for the shell's notification capability list. The shell tells the napplet which notification features it supports (toasts, badges, actions, channels, system notifications). Returns a Subscription with close().
Wire Protocol
notify.* messages use the NIP-5D wire format ({ "type": "domain.action", ...payload }).
| Type | Direction | Payload fields |
|---|---|---|
notify.send |
napplet -> shell | id, title, body?, icon?, actions?, channel?, priority? |
notify.send.result |
shell -> napplet | id, notificationId?, error? |
notify.dismiss |
napplet -> shell | notificationId |
notify.badge |
napplet -> shell | count |
notify.channel.register |
napplet -> shell | channelId, label, description?, defaultPriority? |
notify.permission.request |
napplet -> shell | id, channel? |
notify.permission.result |
shell -> napplet | id, granted |
notify.action |
shell -> napplet | notificationId, actionId |
notify.clicked |
shell -> napplet | notificationId |
notify.dismissed |
shell -> napplet | notificationId, reason? |
notify.controls |
shell -> napplet | controls |
Key design notes:
notify.send/notify.send.resultuseidfor correlation. The shell assignsnotificationIdon success.notify.dismiss,notify.badge, andnotify.channel.registerare fire-and-forget (noidcorrelation).notify.permission.request/notify.permission.resultuseidfor correlation. The shell MAY prompt the user asynchronously.notify.action,notify.clicked, andnotify.dismissedare shell-initiated. The shell sends these when the user interacts with a notification.notify.controlsis shell-initiated. The shell pushes its supported notification capabilities.
Priority
Four priority levels control notification urgency:
| Priority | Behavior hint |
|---|---|
low |
Silent, no interruption. May appear in notification center only. |
normal |
Default. Standard toast or notification. |
high |
Prominent display. May use sound or vibration. |
urgent |
Immediate attention required. May bypass do-not-disturb. |
The shell ultimately decides how each priority level maps to its notification UI.
Actions
Notifications may include up to 3 action buttons:
{
"actions": [
{ "id": "reply", "label": "Reply" },
{ "id": "dismiss", "label": "Dismiss" }
]
}
When the user clicks an action, the shell sends notify.action with the notificationId and actionId. The shell MAY limit the number of actions displayed.
Examples
Send a notification:
-> { "type": "notify.send", "id": "n1", "title": "New message", "body": "Alice: hey!", "priority": "normal" }
<- { "type": "notify.send.result", "id": "n1", "notificationId": "shell-42" }
Send with actions:
-> { "type": "notify.send", "id": "n2", "title": "Incoming call", "priority": "urgent", "actions": [{ "id": "answer", "label": "Answer" }, { "id": "decline", "label": "Decline" }] }
<- { "type": "notify.send.result", "id": "n2", "notificationId": "shell-43" }
User clicks an action:
<- { "type": "notify.action", "notificationId": "shell-43", "actionId": "answer" }
User clicks notification body:
<- { "type": "notify.clicked", "notificationId": "shell-42" }
Notification auto-dismissed:
<- { "type": "notify.dismissed", "notificationId": "shell-42", "reason": "timeout" }
Set badge count:
-> { "type": "notify.badge", "count": 3 }
Clear badge:
-> { "type": "notify.badge", "count": 0 }
Register a channel:
-> { "type": "notify.channel.register", "channelId": "messages", "label": "Messages", "description": "Direct messages and mentions", "defaultPriority": "normal" }
Request permission:
-> { "type": "notify.permission.request", "id": "p1", "channel": "messages" }
<- { "type": "notify.permission.result", "id": "p1", "granted": true }
Dismiss a notification:
-> { "type": "notify.dismiss", "notificationId": "shell-42" }
Shell sends control list:
<- { "type": "notify.controls", "controls": ["toasts", "badges", "actions", "channels"] }
Notification rejected:
<- { "type": "notify.send.result", "id": "n3", "error": "permission denied" }
Error Handling
notify.send.result includes an error field (string) on failure. Common errors: "permission denied", "rate limited", "invalid channel". When error is present, notificationId is absent and the notification was not created.
notify.permission.result returns granted: false if the shell denies permission. This is not an error -- it is a normal policy response.
Messages referencing an unknown notificationId (e.g., notify.dismiss for an already-dismissed notification) MUST be silently ignored by both napplet and shell.
Shell Behavior
- The shell MUST accept
notify.sendmessages and respond withnotify.send.resultcarrying the sameid. - The shell MUST accept
notify.permission.requestmessages and respond withnotify.permission.resultcarrying the sameid. - The shell MUST track active notifications per napplet and remove them when the napplet iframe is removed.
- The shell SHOULD display notifications using its own UI (toasts, system notifications, notification center).
- The shell SHOULD respect the
priorityfield when deciding notification urgency and display style. - The shell MAY send
notify.action,notify.clicked, andnotify.dismissedmessages when users interact with notifications. - The shell MAY send
notify.controlsto inform the napplet which notification features the shell supports. - The shell MAY enforce notification rate limits per napplet.
- The shell MAY enforce ACL checks on
notifycapabilities (e.g., restricting which napplets can send notifications). - The shell MAY support notification channels and allow users to configure per-channel policies.
- The shell MAY delegate to the browser's Notification API for system-level notifications when the shell has permission.
- The shell MUST silently ignore messages referencing unknown notification IDs.
- The shell MUST clean up all notifications for a napplet when the napplet's iframe is removed.
Security Considerations
- Notifications expose user-facing text content. The shell SHOULD sanitize notification
titleandbodyto prevent injection attacks (HTML, scripts). - The
iconfield is a URL or identifier fetched by the shell. The shell SHOULD validate URLs and enforce a content security policy for fetched resources. - Notification actions allow napplets to present choices to users. The shell SHOULD clearly attribute notifications to the originating napplet to prevent spoofing (e.g., a malicious napplet pretending to be a system dialog).
- The
urgentpriority level could be abused for attention-grabbing spam. The shell SHOULD rate-limit urgent notifications and MAY require explicit user consent for urgent notifications. - Permission requests are asynchronous and may involve user prompts. The shell MUST NOT block other napplet messages while a permission prompt is pending.
- Badge counts are advisory -- the shell displays them but the napplet controls the count. A malicious napplet could set misleading badge counts. The shell MAY cap or ignore badge counts.
Implementations
- (none yet)
Changelog
6312406- Introduced NAP-NOTIFY for shell-rendered notifications.a0a4f08- Adopted injected-domain availability and linked the current upstream web binding.