This NIP defines a way for web apps to communicate with web signers embedded as iframes using browser APIs. It is applicable to self-custodial signers that store keys in the browser's local storage. Basically, client app does nip-46 RPC with an iframe using a [`MessageChannel`](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel).
Due to privacy-related restrictions, local storage of iframes is [partitioned](https://developer.mozilla.org/en-US/docs/Web/Privacy/State_Partitioning) from top-frame's storage - iframe of signer.com can't see the storage of top-level signer.com and can't access user's private key. Solution to this challenge is the biggest part of this NIP.
Advantage: minimal latency, no relay involved, no reliance on unreliable webpush, fully cross-platform. User private key is as safe in iframe's local storage as in top-level storage due to browsers' cross-origin restrictions.
-`signer`: non-custodial signer storing keys in it's local storage in the browser
-`top-level signer`: signer opened in a separate tab/popup
-`iframe signer`: signer opened as an iframe inside the client
-`worker iframe`: iframe signer that handles nip46 calls from the client
-`starter iframe`: iframe signer that launches the connection flow to get user confirmation and import keys from top-level signer to iframe signer
-`rebinder iframe`: iframe signer that launches re-connection flow to check that client has access to keys at the top-level signer and to re-import the keys to iframe signer
Signer MAY signal support for this NIP by adding `nip46.iframe_url` field in their `/.well-known/nostr.json?name=_` file or their nip89 `kind:31990` event.
To initiate a connection, `client` shows the `starter iframe`. After a [`user gesture`](https://developer.mozilla.org/en-US/docs/Web/API/UserActivation), `starter iframe` interacts with `top-level signer`, acquires user confirmation and imports the private key to `iframe signer` storage.
If `worker iframe` receives a request but has no target keys due to _ephemeral_ partitioned storage ([Brave](https://brave.com/privacy-updates/7-ephemeral-storage/), Webkit), it pauses the call and sends an error code to the `client`, which then shows the `rebinder iframe`. After a `user gesture`, `rebinder iframe` interacts with `top-level signer` and if `client` is already approved - imports the private key to `iframe signer` storage. At this point `worker iframe` can retry the paused call and send the reply back to `client`.
If request must be confirmed by the user, `worker iframe` sends `auth_url` nip46 response, which is presented by `client` to the user as a visible `checker iframe`, instead of a popup as in nip46. After user confirms, `worker iframe` sends reply to `client`.
This NIP covers interactions between `client` and `iframe signers`. Interactions of `signer` frames (top-level/iframes) are up to implementations (see Appendix for recommendations).
## Starter iframe
To initiate a connection, a `starter iframe` MAY be embedded by the `client`.
`starter iframe` MUST be served when `iframe_url` has `?connect=` parameter set to `nostrconnect://` string defined in nip46.
`starter iframe` SHOULD display a button suggesting the users to `Continue`, recommended dimentions are up to `180px` width and `80px` height.
If user clicks `Continue`, `starter iframe` SHOULD create a `top-level signer` popup and interact with it to acquire the private key if user confirms, details are up to implementations.
When `starter iframe` has finished importing the user private key, it MUST notify the `client` using [`postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage).
The message `data` will be an `Array` with a `starterDone` string and a nip46 `connect`-method reply object (created with a `secret` value from `nostrconnect://` string):
To receive messages from iframes, `client` listens to `message` events.
When `client` receives a message whose `data` field is an `Array` AND has first element equal to `starterDone` AND has second element equal to valid `connect` reply object AND has message `origin` equal to `iframe_url` origin it MAY destroy the `starter iframe` and assume connection is established.
`pubkey` from `connect` reply event is nip46 `remote signer pubkey`.
In case of error, `starter iframe` SHOULD send a message of `["starterError", "Error text"]`, so that client could notify the user.
`client` SHOULD save the `iframe_url` to local storage and reuse it until logout, to make sure the `worker iframe` is loaded from the same origin that was used when connection was established.
When `client` receives a message whose `data` field is an `Array` AND has first element equal to `workerReady` AND has message `origin` equal to `iframe_url` origin it MAY use the `port` from the second `data` element to send nip46 request event objects to it, and then MUST listen to replies from the same `port`:
`MessageChannel` is used instead of `window.postMessage` to let `worker iframe` pass it's port to it's request-processing component (service worker etc) and have `client` talk directly to it without additional latency.
In case of initialization error, `worker iframe` MAY send a message of `["workerError", "Error text"]` instead of the `workerDone`, so that client could notify the user.
When `worker iframe` receives a nip46 request targeting user pubkey that it doesn't have (local storage deleted) it MAY pause the call and then MUST reply with a string `errorNoKey:<requestEvent.id>` to notify the `client`.
If `client` receives `errorNoKey:<requestEvent.id>` reply matching one of pending requests, it MAY display a `rebinder iframe`.
`rebinder iframe` MUST be served when `iframe_url` has `?rebind=<client_pubkey>&pubkey=<remote_signer_pubkey>` parameters.
`rebinder iframe` SHOULD display a button suggesting the users to `Continue`, recommended dimentions are up to `180px` width and `80px` height.
If user clicks `Continue`, `rebinder iframe` SHOULD create a `top-level signer` popup and interact with it to acquire the private key if `client_pubkey` is already permitted to access the `remote_signer_pubkey`, details are up to implementations.
When `rebinder iframe` has finished importing the user private key, it MUST notify the `client` using `postMessage`. The message `data` will be `["rebinderDone"]`:
```
// rebinder iframe
window.parent.postMessage(["rebinderDone"], "*");
```
When `client` receives a message whose `data` field is an `Array` AND has first element equal to `rebinderDone` AND has message `origin` equal to `iframe_url` origin it MAY destroy the `rebinder iframe`.
```
// client
window.addEventListener("message", (msg) => {
if (Array.isArray(msg.data)
&& msg.data[0] === "rebinderDone"
&& msg.origin === originOf(iframe_url)
) {
// destroy rebinder iframe
}
})
```
In case of error, `rebinder iframe` SHOULD send a message of `["rebinderError", "Error text"]`, so that client could notify the user.
When `worker iframe` that has paused the request notices that user private key was imported by `rebinder iframe` into the local storage, it MUST restart the paused call and deliver the reply to the client.
`auth_url` MAY be presented by `client` to the user, and in that case MUST be shown in a visible `checker iframe` instead of a popup as in nip46 to provide access to `iframe signer` storage. Recommended dimentions are minimum `300px` width and `600px` height.
After user confirms the request in `checker iframe`, `worker iframe` MUST resume the paused call and deliver the reply to the client.
After reply is received from `worker iframe`, `client` SHOULD destroy the `checker iframe`.
```mermaid
sequenceDiagram
participant Client
participant Worker as Worker iframe
participant Checker as Checker iframe
Note over Client,Checker: Client webpage
Client ->>+ Worker: nip46 request
Worker -->> Worker: need confirm?
Worker ->> Client: auth_url
Client ->>+ Checker: embed checker iframe
Note over Worker,Checker: Request in iframe signer storage
Checker -->> Checker: user confirms
Note over Worker,Checker: Reply in iframe signer storage
-`starter iframe` and `rebinder iframe` MAY open a `top-level signer` in a popup after user gesture (click on Continue) and MAY then send messages to it using `postMessage`.
- after user confirms in the `top-level signer` it MAY send the private key back to it's `window.opener`
-`signer` frames should check `origin` of received messages to make sure they're talking to each other
- popups SHOULD be opened using `window.open(url, "<random_target>", "<options>")`, `<random_target>` is required (instead of `_blank`) to make sure popup has access to `opener`, `noopener` SHOULD NOT be provided for the same reason.
-`iframe signers` SHOULD NOT launch a popup until their service worker has started, it seems like Safari pauses iframe's SW initialization if the tab loses focus
-`iframe signers` MAY be sandboxed, but then MUST at least have `allow-scripts,allow-same-origin,allow-popups-to-escape-sandbox`
-`starter iframe` SHOULD be provided access to `referrer` to let the signer use it as client name/url.
-`iframe signers` MAY use `MessageChannel` to create a port that will be transferred to `top-level signer` and to it's service-worker so that it could then export the user private key to the `iframe signer`.
-`signer` SHOULD use random auxiliary sub-domains to serve `iframe_url` - Chrome desktop allows users to delete signer's storage on client's tab, which (mistakenly?) removes top-level signer storage too and users' keys might be lost.
-`top-level signer` MAY export `client pubkey` to `iframe signer` along with user private key to scope the `iframe signer` to this particular connection.