Add code examples, minor fixes

This commit is contained in:
artur 2024-10-31 14:56:45 +01:00
parent 0a2eb9df5c
commit 2f199e1cdc
2 changed files with 91 additions and 183 deletions

101
146.md
View File

@ -12,7 +12,7 @@ Due to privacy-related restrictions, local storage of iframes is [partitioned](h
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. 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.
Disadvantage: potentially more frequent "confirmations", higher surface for web-based hacks. Disadvantage: potentially more frequent "confirmations", bigger surface for web-based hacks.
## Terms ## Terms
@ -26,15 +26,15 @@ Disadvantage: potentially more frequent "confirmations", higher surface for web-
## Overview ## Overview
When nip46 connection is established, client embeds invisible `worker iframe` and exchanges nip46 messages with it using `postMessage`. When nip46 connection is established, `client` embeds invisible `worker iframe` and exchanges nip46 messages with it using `postMessage`.
To overcome partitioned storage, when client initiates nip46 connection and receives `auth_url`, it shows the `starter iframe` with "Continue" button. After user clicks, starter iframe launches auth_url popup to access top-level signer. When user confirms, top-level signer sends the user private key back to starter iframe using postMessage. User private key can then be used by `worker iframe`. To overcome partitioned storage, when client initiates nip46 connection and receives `auth_url`, it shows the `starter iframe` with "Continue" button. After user clicks, starter iframe launches auth_url popup to access `top-level signer`. When user confirms, top-level signer sends the user private key back to starter iframe using postMessage. User private key can then be used by worker iframe.
To overcome ephemeral partitioned storage ([Brave](https://brave.com/privacy-updates/7-ephemeral-storage/), Webkit), when worker iframe receives a request but has no target keys, it pauses the call and sends an error code. Client shows visible `rebinder iframe` with "Continue" button. When user clicks, rebinder iframe launches top-level signer in a popup, which checks if client pubkey has access to the user pubkey and if so sends user private key back to rebinder iframe, without user having to confirm anything. To overcome _ephemeral_ partitioned storage ([Brave](https://brave.com/privacy-updates/7-ephemeral-storage/), Webkit), when worker iframe receives a request but has no target keys, it pauses the call and sends an error code. Client shows `rebinder iframe` with "Continue" button. When user clicks, rebinder iframe launches top-level signer in a popup, which checks if client pubkey has access to the user pubkey and if so sends user private key back to rebinder iframe, without user having to confirm anything.
## Worker iframe ## Worker iframe
Signer may signal support for this NIP by adding `nip46.iframe_url` field in their nostr.json file. If client detects a signer that supports iframes, after the connection has already been established, the client MAY create an invisible `worker` iframe with `src` set to `nip46.iframe_url`. Signer may signal support for this NIP by adding `nip46.iframe_url` field in their `/.well-known/nostr.json?name=_` file. If client detects a signer that supports iframes, after the connection has already been established, the client MAY create an invisible `worker iframe` with `src` set to `nip46.iframe_url`.
When worker iframe has started and is ready to process requests it must notify the client by calling `window.parent.postMessage("workerReady", "*")`. In turn, the client must subscribe to worker's messages with `window.addEventListener("message", handler)` before creating an iframe, and must wait for the message event with `data` set to `workerReady` and `origin` set to origin of `nip46.iframe_url` before sending requests. When worker iframe has started and is ready to process requests it must notify the client by calling `window.parent.postMessage("workerReady", "*")`. In turn, the client must subscribe to worker's messages with `window.addEventListener("message", handler)` before creating an iframe, and must wait for the message event with `data` set to `workerReady` and `origin` set to origin of `nip46.iframe_url` before sending requests.
@ -44,26 +44,107 @@ Worker iframe processes the request and sends back the reply event by using `req
## Starter iframe ## Starter iframe
Due to local storage partitioning, an iframe must import the needed key from the top-level storage of the signer. To do that, iframe must obtain a reference to a top-level signer and exchange messages. This fits well with the need to open top-level signer popup with `auth_url` of the nip-46 connection flow. However, this time the popup opener must be the signer iframe, and it must follow a [user gesture](https://developer.mozilla.org/en-US/docs/Web/API/UserActivation) in iframe. Due to local storage partitioning, an iframe must import the needed key from the top-level storage of the signer. To do that, `iframe signer` must obtain a reference to a `top-level signer` and exchange messages. This fits well with the need to open top-level signer popup with `auth_url` of the nip-46 connection flow. However, this time the popup opener must be the `signer iframe`, and it must follow a [user gesture](https://developer.mozilla.org/en-US/docs/Web/API/UserActivation) in iframe.
To achieve that, a `starter iframe` MAY be embedded by the client when `auth_url` is received while the client is connecting. Starter iframe MUST be served when `nip46.iframe_url` has `?auth_url=<auth_url>` parameter, and MUST display a button suggesting the users to `Continue`. Recommended button dimentions SHOULD be up to `180px` width and `80px` height. To achieve that, a `starter iframe` MAY be embedded by the client when `auth_url` is received while the client is connecting. Starter iframe MUST be served when `nip46.iframe_url` has `?auth_url=<auth_url>` parameter, and MUST display a button suggesting the users to `Continue`. Recommended button dimentions SHOULD be up to `180px` width and `80px` height.
When user clicks on `Continue` button in starter iframe, it MUST open the popup with `auth_url` and MUST accept the user's private key if user confirms in the popup. Details are implementation-defined, see recommendations below. When user clicks on `Continue` button in starter iframe, it MUST open the popup with `auth_url` and MUST accept the user's private key if user confirms in the popup. Details are implementation-defined, see recommendations below.
When starter iframe has finished importing the user private key from the popup, it MUST notify the app by calling `window.parent.postMessage("starterDone", "*")`. When client receives the `starterDone` from iframe_url's origin it MAY destroy the starter iframe and MAY create the worker iframe to start processing requests. When starter iframe has finished importing the user private key from the popup, it MUST notify the app by calling `window.parent.postMessage("starterDone", "*")`. When client receives the `starterDone` from iframe_url's origin it MAY destroy the starter iframe and MAY create the `worker iframe` to start processing requests.
## Rebinder iframe ## Rebinder iframe
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. 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 `nip46.iframe_url` has `&rebind=<local_pubkey>&pubkey=<remote_user_pubkey>` parameters, and MUST display a button similar to starter iframe suggesting the users to `Continue`. If client receives `errorNoKey:<requestEvent.id>` reply matching one of pending requests, it MAY display a `rebinder iframe`. Rebinder iframe MUST be served when `nip46.iframe_url` has `&rebind=<client_pubkey>&pubkey=<remote_signer_pubkey>` parameters, and MUST display a button similar to starter iframe suggesting the users to `Continue`.
When user clicks on `Continue` button in rebinder iframe, it MUST open a top-level signer popup. The popup MUST check that provided `local_pubkey` has permissions to access `remote_user_pubkey` and then MUST pass the user private key to rebinder iframe. Details are implementation-defined, see appendix for recommendations. When user clicks on `Continue` button in rebinder iframe, it MUST open a `top-level signer` popup. The popup MUST check that provided `client_pubkey` has permissions to access `remote_signer_pubkey` and then MUST pass the user private key to rebinder iframe. Details are implementation-defined, see appendix for recommendations.
After rebinder iframe has finished importing the user private key, it MUST notify the client by calling `window.parent.postMessage("rebinderDone", "*")`. When client receives the `rebinderDone` from iframe_url's origin it MAY destroy the rebinder iframe. After rebinder iframe has finished importing the user private key, it MUST notify the client by calling `window.parent.postMessage("rebinderDone", "*")`. When client receives the `rebinderDone` from iframe_url's origin it MAY destroy the rebinder iframe.
When worker iframe that has paused the request notices that user private key was imported by rebinder into the local store, it MUST restart the paused call and deliver the reply to the client. When worker iframe that has paused the request notices that user private key was imported by rebinder into the local store, it MUST restart the paused call and deliver the reply to the client.
## Client Pseudocode
### Worker iframe usage
```
const iframeOrigin = new URL(iframeUrl).origin;
// helper
const getReply = async () => {
return new Promise(ok => {
const handler = (e) => {
if (e.origin !== iframeOrigin) return;
window.removeEventListener("message", handler)
ok(e.data);
}
window.addEventListener("message", handler)
})
};
// create invisible iframe
const iframe = createIframe(iframeUrl, 'style="display: none"')
// wait for workerReady message
while (await getReply() !== "workerReady");
// send nip46 request event
const nip46Req = await createReq("sign_event", {...});
iframe.contentWindow.postMessage(nip46Req, iframeOrigin);
// get reply
const nip46Reply = await getReply();
// process reply
...
```
### Starter iframe usage
```
const authUrl = await connectGetAuthUrl(...);
// display starter iframe
const iframe = createIframe(`${iframeUrl}?auth_url=${authUrl}`, 'style="width: 180px; height: 80px')
// wait till it's done
while (await getReply() !== "starterDone");
// can delete starter
deleteIframe(iframe);
// create worker to process requests
...
```
### Rebinder iframe usage
```
// worker iframe returns error
let workerReply = await getReply()
if (workerReply.startsWith("errorNoKey:")) {
// display starter iframe
const iframe = createIframe(`${iframeUrl}?rebind=${clientPubkey}&pubkey=${remoteSignerPubkey}`, 'style="width: 180px; height: 80px')
// wait till it's done
while (await getReply() !== "rebinderDone");
// can delete rebinder
deleteIframe(iframe);
// re-fetch reply
workerReply = await getReply();
}
// process worker reply
...
```
## Recommendations ## Recommendations

173
512.md
View File

@ -1,173 +0,0 @@
Nostr Sites
===========
This NIP describes a way to render a selection of Nostr events on a website.
A web page that is part of the Nostr Site must include `nostr:site` meta tag:
```
<meta property="nostr:site" content="<naddr>" />
```
The `site` event, referenced by `naddr` from `nostr:site` meta tag, is a parameterized replaceable event of kind `30512`. The `content` field MAY contain a rich description of the site in markdown syntax (NIP-23), all tags are optional (except `d`).
Tags:
```
{
"tags":[
// absolute url of the nostr site root, may include /path/, must not include query string, must end with /
["r", "https://site.com/"],
["name", "<pwa name>"],
["title", "<site title>"],
["summary", "<site description>"],
["image", "<site code image url>"],
// contributors, if omitted - event's author is the single site contributor
["p", "<contributor-pubkey>"],
// filters for fetching published events
// <tag> - single-letter tag
["include", "<tag>", "<value>"],
// list of included event kinds
["kind", "30023"],
// override contributors' outbox relays to fetch content
["relay", "<relay>"],
// event id and package hash of the extensions (themes, plugin)
["x", "<id>", "<relay>", "<package-hash>", "<petname>"],
// renderer engine, preferably reverse-domain notation, should match theme engine and plugin engine, i.e. `pro.npub.v1`
["z", "<engine>"],
// meta tags for website rendering, seo, social, navigation
["icon", "<favicon url>"],
["logo", "<header logo url>"],
["color", "<#hex - accent color, PWA theme_color>"],
["lang", "<language code>"],
["meta_title", "<overrides title>"],
["meta_description", "<overrided summary>"],
["og_title", "<open-graph title, overrides title>"],
["og_description", "<open-graph description, overrides summary>"],
["og_image", "<open-graph image url, overrides image>"],
["twitter_image", "<twitter image url, overrides image>"],
["twitter_title", "<twitter title, overrides title>"],
["twitter_description", "<twitter description, overrides title>"],
// primary navigation, one tag per link
["nav", "</relative/url>", "<label>"],
]
}
```
By default, no events are published on the site.
Admin MAY use `include` tags to specify which events, authored by contributors, should be displayed on the site. `include` contains a `key:value` pair of single-letter tag and it's value that will be used as filters to fetch published events, i.e. `t:bitcoin` or `g:<geohash>`. A special value of `*` means "everything".
If `kind` tags are specified, only these kinds will be fetched.
If `relay` tags are specified, only these relays will be used, overriding the outbox relays of the contributors.
If `include` has a special value of `?` then "manual" submission is enabled and contributors MAY create `submit` events that reference the target events to be published on the site.
"Submit" events
===============
Events of kind `512` are used by Nostr Site contributors to submit an event to the site (manual submission must be enabled with `include=?`). `content` field may be empty, tags may include:
```
{
"tags":[
["a", "<addr>", "<relay>", "site"], // addr of the target site
["a"/"e", "<addr/id>", <relay>], // addr/id of the submitted event
["a"/"e", "<addr/id>", <relay>], // addr/id of the submitted event
["k", "<kind>"], // submitted event's kind, to allow filtering by target event's kind
["r", "<slug>"] // optional slug for submitted event, to be used in the website url, i.e. /posts/<slug>.
]
}
```
Only zero or one event may be referenced with `a` or `e` tag.
If `relay` tags are specified in `site` event, `submit` events will only be fetched from those relays.
If target event is authored by none of the contributors, it should be rendered as a `repost` by the contributor and show the original author.
If `r` tag's value starts with `/` then this a relative url of a `static page` (i.e. `r:/path/to/static/page`). The submitted events referenced by `a` or `e` tags of `static pages` SHOULD NOT be included when listing published events, but are only rendered at the specified url. If no `a` or `e` tags are specified in a `submit` event, then it's `content` field will be rendered as the content of the static page - this allows to exclude such static pages from social media feeds.
If several `submit` events published by contributors have the same `r` tag then the most recent one should be preferred.
To override default meta tags of a page, `submit` event may include meta info tags of the `site` event (`title`, `image`, `meta_title`, `og_title` etc).
Hashtag pages
=============
Sites may need to display additional info on web pages dedicated to hashtags, and may use `hashtag` event published by contributors for that. The `hashtag` event is a parameterized replaceable event of kind `30513`. the `content` field may include a string of text in the same format as "long-form content" NIP-23, to be used as the body of the hashtag web page. Hashtag event will have tags:
```
{
"tags":[
["t", "<hashtag>"], // target hashtag, must be included exactly once
["a", "<addr>"], // nostr site address
["r", "<slug>"], // optional slug for hashtag, to be used in the website url, i.e. /tags/<slug>.
]
}
```
To override default meta tags of a hashtag page, `hashtag` event may include meta info tags of the `site` event (`title`, `image`, `meta_title`, `og_title` etc).
If `r` tag starts with `/` then it is a static hashtag webpage, and this hashtag should not be listed under posts and in other hashtag lists.
If several `hashtag` events published by contributors have the same `r` tag then the most recent one should be preferred.
Themes and Plugins
==================
The `theme` event is parameterized replaceable event of kind `30514`, it's `content` field may have a "rich" description in the same syntax as NIP-23 long-form notes. It has the following tags:
```
{
"tags":[
["title", "<theme name>"],
["summary", "<theme description>"],
["version", "<latest version>"],
["license", "<theme license>"], // MIT, etc
["e", "<package id>", "<relay>"], // theme code package event id
["z", "<website engine id>"], // engine, must match the nostr site engine
]
}
```
The `plugin` event is parameterized replaceable event of kind `30515` with the same structure.
The `package` event of kind `1036` (see the new NIP-136) is essentially a directory of files - relative urls and hashes. It may contain release notes, and also contains the `package hash` - a combined hash of all files
and their relative urls.
The `package` event of kind `1036` SHOULD have additional tags:
```
{
"tags":[
["l", "theme|plugin", "org.nostrsites.ontology"], // type of code
["L", "org.nostrsites.ontology"], // label category
["a", "<addr>"] // address of the theme or plugin replaceable event
]
}
```
The `site` events are linked to the themes and plugins using package event ids, and also the `package hash` (see `site` event above). This will allow us to handle link rot - if theme package event is deleted by relays, other events with the same `package hash` can be found to avoid disruption of every site using the deleted package.
Rendering
=========
Steps to render a Nostr Site to produce an HTML page:
- parse site event `naddr` from meta-tags of the HTML fetched from the server (or from settings for server-side rendering)
- fetch `site event` from relays specified in `naddr`
- if not found, fall back to fetching `naddr` author's outbox relays
- fetch extensions (plugins and themes)
- fetch authors' relays (will be omitted if single admin-author)
- if `include` tags are specified then
- fetch by tags from those authors from their outbox relays
- also if `include="?"` is specified:
- fetch `submit` events from authors' outbox relays
- fetch targets (may include fetching target authors' relays)
- init template engine and render the target events into html
- if root url is specified, renderer should put all internal links as sub-path to root