Update docs for app session
This commit is contained in:
+13
-12
@@ -6,13 +6,13 @@ A comprehensive framework for building nostr clients, powering production applic
|
||||
|
||||
## What's Included
|
||||
|
||||
- **Repository System** - Event storage and query capabilities
|
||||
- **Router** - Intelligent relay selection for optimal networking
|
||||
- **Feed Controller** - Manages feed creation and updates
|
||||
- **Repository** - Event storage and query capabilities
|
||||
- **Router** - Intelligent relay selection for optimal network access
|
||||
- **Feed Controller** - Manages feed loading
|
||||
- **Session Management** - User identity and key management
|
||||
- **Event Actions** - High-level operations like reacting, replying, etc.
|
||||
- **Profile Management** - User profile handling and metadata
|
||||
- **Relay Directories** - Discovery and management of relays
|
||||
- **Relay Directory** - Discovery and management of relays
|
||||
- **Web of Trust** - Utilities for building webs of trust
|
||||
|
||||
## Quick Example
|
||||
@@ -61,18 +61,19 @@ const events = await load({
|
||||
}])
|
||||
|
||||
// Or use `request` for more fine-grained subscription control
|
||||
const req = request({
|
||||
const abortController = new AbortController()
|
||||
|
||||
request({
|
||||
signal: abortController.signal,
|
||||
relays: Router.get().ForUser().getUrls(),
|
||||
filters: [{kinds: [NOTE],
|
||||
onEvent: (event: TrustedEvent) => {
|
||||
console.log(event)
|
||||
},
|
||||
}])
|
||||
|
||||
// Listen for events
|
||||
req.on(RequestEvent.Event, (event: TrustedEvent) => {
|
||||
console.log(event)
|
||||
})
|
||||
|
||||
// Close the req
|
||||
req.close()
|
||||
// Close the request
|
||||
abortController.abort()
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
+163
-66
@@ -6,6 +6,7 @@ The session system provides a unified way to handle different authentication met
|
||||
- NIP-07 via Browser Extension
|
||||
- NIP-46 via Bunker URL or Nostrconnect
|
||||
- NIP-55 via Android Signer Application
|
||||
- Read-only pubkey login
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -16,96 +17,192 @@ Sessions are stored in local storage and can be:
|
||||
- Switched dynamically
|
||||
- Backed by different signing methods
|
||||
|
||||
## Basic Usage
|
||||
## NIP 01 Example
|
||||
|
||||
The simplest type of login is NIP 01, although it's generally a bad idea to be handling user keys. NIP 46, 44, or 07 login are preferable. However, NIP 01 can be useful for supporting signup, local profiles, or ephemeral keys.
|
||||
|
||||
```typescript
|
||||
import {sessions, pubkey, addSession, dropSession} from '@welshman/app'
|
||||
import {makeSecret} from '@welshman/signer'
|
||||
import {loginWithNip01} from '@welshman/app'
|
||||
|
||||
// Add multiple sessions
|
||||
addSession({method: 'nip07', pubkey: 'abc...'})
|
||||
addSession({method: 'nip46', pubkey: 'def...', secret: '123'})
|
||||
|
||||
// Switch between sessions
|
||||
pubkey.set('abc...') // Activates that session
|
||||
|
||||
// Remove a session
|
||||
dropSession('abc...')
|
||||
|
||||
// List all sessions
|
||||
console.log(sessions.get())
|
||||
loginWithNip01(makeSecret())
|
||||
```
|
||||
|
||||
## NIP-46 (Bunker) Authentication
|
||||
## NIP 07 Example
|
||||
|
||||
A simple way to sign in for desktop browser users is using [NIP 07](https://github.com/nostr-protocol/nips/blob/master/07.md). This method is easy to implement, but should be used sparingly, since not all users will be using a browser with a nostr signing extension installed.
|
||||
|
||||
```typescript
|
||||
import {Nip46Broker, Nip46Signer} from '@welshman/signer'
|
||||
import {addSession} from '@welshman/app'
|
||||
import {Nip07Signer} from '@welshman/signer'
|
||||
import {loginWithNip07} from '@welshman/app'
|
||||
|
||||
// Connect to a bunker
|
||||
const clientSecret = makeSecret()
|
||||
const relays = ['wss://relay.damus.io']
|
||||
const broker = Nip46Broker.get({relays, clientSecret})
|
||||
const signer = new Nip07Signer()
|
||||
|
||||
// Generate nostrconnect URL for the bunker
|
||||
const connectUrl = await broker.makeNostrconnectUrl({
|
||||
name: "My App",
|
||||
url: "https://myapp.com"
|
||||
})
|
||||
|
||||
// Wait for user to approve in bunker
|
||||
const response = await broker.waitForNostrconnect(connectUrl)
|
||||
|
||||
// Create session
|
||||
addSession({
|
||||
method: 'nip46',
|
||||
pubkey: response.event.pubkey,
|
||||
secret: clientSecret,
|
||||
handler: {
|
||||
pubkey: response.event.pubkey,
|
||||
relays
|
||||
signer.getPubkey().then(pubkey => {
|
||||
if (pubkey) {
|
||||
loginWithNip07(pubkey)
|
||||
} else {
|
||||
// User extension does not exist or did not respond
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Using Session Signer
|
||||
## NIP-46 Authentication
|
||||
|
||||
The best default signing scheme is [NIP 46](https://github.com/nostr-protocol/nips/blob/master/46.md), AKA "Nostr Connect". This supports multiple handshakes depending on desired UX, and can support advanced use cases like secure enclaves, self-hosted keys, and FROST multisig.
|
||||
|
||||
The simpler `bunker://` handshake is done by asking the user to provide a bunker URL, either by QR code, or by pasting it manually into your application.
|
||||
|
||||
```typescript
|
||||
import {Nip46Broker, makeSecret} from "@welshman/signer"
|
||||
import {loginWithNip46, nip46Perms} from "@welshman/app"
|
||||
import {isKeyValid} from "src/util/nostr"
|
||||
|
||||
// Make a client secret - this is distinct from the user's private key, and is used
|
||||
// for communicating securely with the remote signer
|
||||
const clientSecret = makeSecret()
|
||||
|
||||
// Ask the user to input their bunker URL
|
||||
const bunkerUrl = prompt("Please enter your bunker url")
|
||||
|
||||
// Pase the bunker url
|
||||
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl(bunkerUrl)
|
||||
|
||||
if (!isKeyValid(signerPubkey)) {
|
||||
alert("Sorry, but that's an invalid public key.")
|
||||
} else if (relays.length === 0) {
|
||||
alert("That connection string doesn't have any relays.")
|
||||
} else {
|
||||
// Open up a connection with the signer
|
||||
const broker = Nip46Broker.get({relays, clientSecret, signerPubkey})
|
||||
|
||||
// Send a connect request with the default permissions
|
||||
const result = await broker.connect(connectSecret, nip46Perms)
|
||||
|
||||
// Make sure to check the connect secret to prevent hijacking
|
||||
if (result === connectSecret) {
|
||||
// Get the user's public key
|
||||
const pubkey = await broker.getPublicKey()
|
||||
|
||||
if (!pubkey) {
|
||||
alert("Failed to initialize session")
|
||||
} else {
|
||||
loginWithNip46(pubkey, clientSecret, signerPubkey, relays)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, you can provide the user with a `nostrconnect://` URL which they can copy or scan with their signer. This is a better UX for users using a signer on their mobile phone.
|
||||
|
||||
```typescript
|
||||
import {Nip46Broker, makeSecret} from "@welshman/signer"
|
||||
import {loginWithNip46, nip46Perms} from "@welshman/app"
|
||||
|
||||
// Create a client secret
|
||||
const clientSecret = makeSecret()
|
||||
|
||||
// Stop listening if the user cancels login
|
||||
const abortController = new AbortController()
|
||||
|
||||
// Customize to use relays the signer can send responses to
|
||||
const relays = ['wss://relay.nsec.app/']
|
||||
|
||||
// Create a broker
|
||||
const broker = Nip46Broker.get({clientSecret, relays})
|
||||
|
||||
// Create a nostrconnect:// url
|
||||
const nostrconnect = await broker.makeNostrconnectUrl({
|
||||
name: "My App",
|
||||
url: window.origin,
|
||||
image: window.origin + '/logo.png',
|
||||
perms: nip46Perms,
|
||||
})
|
||||
|
||||
// Share it with the user. Displaying a QR code is particularly helpful
|
||||
alert("To connect, paste this URL into your signer: " + nostrconnect)
|
||||
|
||||
// Listen for the response
|
||||
let response
|
||||
try {
|
||||
response = await broker.waitForNostrconnect(nostrconnect, abortController.signal)
|
||||
} catch (errorResponse: any) {
|
||||
if (errorResponse?.error) {
|
||||
alert(`Received error from signer: ${errorResponse.error}`)
|
||||
} else if (errorResponse) {
|
||||
console.error(errorResponse)
|
||||
}
|
||||
}
|
||||
|
||||
// If we got a response, the broker is already connected and we can log in
|
||||
if (response) {
|
||||
const pubkey = await broker.getPublicKey()
|
||||
|
||||
if (!pubkey) {
|
||||
alert("Failed to initialize session")
|
||||
} else {
|
||||
loginWithNip46(pubkey, clientSecret, response.event.pubkey, relays)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## NIP-55 Authentication
|
||||
|
||||
For the best UX on Android, use [NIP 55](https://github.com/nostr-protocol/nips/blob/master/55.md). Note that this only works for web applications that have been compiled to native Android applications using [CapacitorJS](https://capacitorjs.com/) and [nostr-signer-capacitor-plugin](https://github.com/chebizarro/nostr-signer-capacitor-plugin).
|
||||
|
||||
```typescript
|
||||
import {getNip55, Nip55Signer, loginWithNip55} from "@welshman/signer"
|
||||
|
||||
// Query for installed apps that implement nip 55 signing
|
||||
getNip55().then(signerApps => {
|
||||
// We'll choose the first one and auto-login, but in most cases you'll want to offer a choice
|
||||
if (signerApps.length > 0) {
|
||||
const signer = new Nip55Signer(signerApps[0].packageName)
|
||||
const pubkey = await signer.getPubkey()
|
||||
|
||||
if (pubkey) {
|
||||
loginWithNip55(pubkey, app.packageName)
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Read-only session
|
||||
|
||||
A fun feature of nostr is that you can log in as other people, and see what nostr is like from their perspective (minus encrypted data or course).
|
||||
|
||||
```typescript
|
||||
import {loginWithPubkey} from "@welshman/signer"
|
||||
|
||||
// Log in as hodlbod
|
||||
loginWithPubkey("97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322")
|
||||
```
|
||||
|
||||
## Using the current session
|
||||
|
||||
```typescript
|
||||
import {signer, session} from '@welshman/app'
|
||||
import {createEvent, NOTE} from '@welshman/util'
|
||||
|
||||
// Print the current session - be aware the private key is stored in memory, be very
|
||||
// careful about how you handle session objects!
|
||||
console.log(session.get())
|
||||
|
||||
// Current session's signer is always ready to use
|
||||
const event = await signer.get().sign(
|
||||
createEvent(NOTE, {content: "Hello Nostr!"})
|
||||
)
|
||||
|
||||
// hodlbod's pubkey
|
||||
const otherPubkey = "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322"
|
||||
|
||||
// Encrypt content for private notes
|
||||
const encrypted = await signer.get().nip44.encrypt(
|
||||
pubkey,
|
||||
"Secret message"
|
||||
)
|
||||
const ciphertext = await signer.get().nip44.encrypt(otherPubkey, "Secret message")
|
||||
|
||||
// Decrypt automatically detects encryption version
|
||||
const plaintext = await decrypt(signer, otherPubkey, ciphertext)
|
||||
```
|
||||
|
||||
## Session Types
|
||||
## Multiple sessions
|
||||
|
||||
```typescript
|
||||
type SessionNip07 = {
|
||||
method: "nip07"
|
||||
pubkey: string
|
||||
}
|
||||
|
||||
type SessionNip46 = {
|
||||
method: "nip46"
|
||||
pubkey: string
|
||||
secret: string
|
||||
handler: {
|
||||
pubkey: string
|
||||
relays: string[]
|
||||
}
|
||||
}
|
||||
|
||||
type SessionNip01 = {
|
||||
method: "nip01"
|
||||
pubkey: string
|
||||
secret: string
|
||||
}
|
||||
```
|
||||
It's possible to support multiple concurrent sessions by simply calling `addSession` multiple times. This will update `sessions`, and set `pubkey` to the most recently added session. You can then switch between sessions by calling `pubkey.set` with a valid session pubkey, and delete sessions using `dropSession(pubkey)`.
|
||||
|
||||
@@ -5,10 +5,9 @@ outline: deep
|
||||
# What is Welshman?
|
||||
|
||||
Welshman is a production-grade nostr toolkit powering [Coracle](https://coracle.social) and [Flotilla](https://flotilla.social).
|
||||
Built as independent, opt-in packages, it lets you choose exactly what you need - from basic event handling to complete feed management.
|
||||
|
||||
Built as independent, opt-in packages, it lets you choose exactly what you need - from basic utilities handling to a batteries-included application framework.
|
||||
|
||||
Need just a content parser? Grab @welshman/content. Building a complex client? Start with @welshman/app and add more packages as you grow.
|
||||
|
||||
Each module is battle-tested in production, designed to work together but never dependent on each other.
|
||||
|
||||
Build your next Nostr client with the same tools that power today's leading Nostr applications.
|
||||
Each module is battle-tested in production, and designed to work together.
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
import {derived} from "svelte/store"
|
||||
import {cached, hash, omit, equals, assoc} from "@welshman/lib"
|
||||
import {withGetter, synced} from "@welshman/store"
|
||||
import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer, Nip55Signer} from "@welshman/signer"
|
||||
import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer, Nip55Signer, getPubkey} from "@welshman/signer"
|
||||
|
||||
export enum SessionMethod {
|
||||
Nip01 = 'nip01',
|
||||
Nip07 = 'nip07',
|
||||
Nip46 = 'nip46',
|
||||
Nip55 = 'nip55',
|
||||
Pubkey = 'pubkey',
|
||||
}
|
||||
|
||||
export type SessionNip01 = {
|
||||
method: "nip01"
|
||||
method: SessionMethod.Nip01
|
||||
pubkey: string
|
||||
secret: string
|
||||
}
|
||||
|
||||
export type SessionNip07 = {
|
||||
method: "nip07"
|
||||
method: SessionMethod.Nip07
|
||||
pubkey: string
|
||||
}
|
||||
|
||||
export type SessionNip46 = {
|
||||
method: "nip46"
|
||||
method: SessionMethod.Nip46
|
||||
pubkey: string
|
||||
secret: string
|
||||
handler: {
|
||||
@@ -25,13 +33,13 @@ export type SessionNip46 = {
|
||||
}
|
||||
|
||||
export type SessionNip55 = {
|
||||
method: "nip55"
|
||||
method: SessionMethod.Nip55
|
||||
pubkey: string
|
||||
signer: string
|
||||
}
|
||||
|
||||
export type SessionPubkey = {
|
||||
method: "pubkey"
|
||||
method: SessionMethod.Pubkey
|
||||
pubkey: string
|
||||
}
|
||||
|
||||
@@ -71,6 +79,42 @@ export const updateSession = (pubkey: string, f: (session: Session) => Session)
|
||||
export const dropSession = (pubkey: string) =>
|
||||
sessions.update($sessions => omit([pubkey], $sessions))
|
||||
|
||||
// Session factories
|
||||
|
||||
export const makeNip01Session = (secret: string): SessionNip01 =>
|
||||
({method: SessionMethod.Nip01, secret, pubkey: getPubkey(secret)})
|
||||
|
||||
export const makeNip07Session = (pubkey: string): SessionNip07 =>
|
||||
({method: SessionMethod.Nip07, pubkey})
|
||||
|
||||
export const makeNip46Session = (pubkey: string, clientSecret: string, signerPubkey: string, relays: string[]): SessionNip46 =>
|
||||
({method: SessionMethod.Nip46, pubkey, secret: clientSecret, handler: {pubkey: signerPubkey, relays}})
|
||||
|
||||
export const makeNip55Session = (pubkey: string, signer: string): SessionNip55 =>
|
||||
({method: SessionMethod.Nip55, pubkey, signer})
|
||||
|
||||
export const makePubkeySession = (pubkey: string): SessionPubkey =>
|
||||
({method: SessionMethod.Pubkey, pubkey})
|
||||
|
||||
// Login utilities
|
||||
|
||||
export const loginWithNip01 = (secret: string) =>
|
||||
addSession(makeNip01Session(secret))
|
||||
|
||||
export const loginWithNip07 = (pubkey: string) =>
|
||||
addSession(makeNip07Session(pubkey))
|
||||
|
||||
export const loginWithNip46 = (pubkey: string, clientSecret: string, signerPubkey: string, relays: string[]) =>
|
||||
addSession(makeNip46Session(pubkey, clientSecret, signerPubkey, relays))
|
||||
|
||||
export const loginWithNip55 = (pubkey: string, signer: string) =>
|
||||
addSession(makeNip55Session(pubkey, signer))
|
||||
|
||||
export const loginWithPubkey = (pubkey: string) =>
|
||||
addSession(makePubkeySession(pubkey))
|
||||
|
||||
// Other stuff
|
||||
|
||||
export const nip46Perms = "sign_event:22242,nip04_encrypt,nip04_decrypt,nip44_encrypt,nip44_decrypt"
|
||||
|
||||
export const getSigner = cached({
|
||||
|
||||
@@ -372,7 +372,7 @@ export class Nip46Broker extends Emitter {
|
||||
return `nostrconnect://${clientPubkey}?${params.toString()}`
|
||||
}
|
||||
|
||||
waitForNostrconnect = (url: string, abort?: AbortController) => {
|
||||
waitForNostrconnect = (url: string, signal: AbortSignal) => {
|
||||
const secret = new URL(url).searchParams.get("secret")
|
||||
|
||||
return makePromise<Nip46ResponseWithResult, Nip46Response | undefined>((resolve, reject) => {
|
||||
@@ -401,7 +401,7 @@ export class Nip46Broker extends Emitter {
|
||||
this.receiver.on(Nip46Event.Receive, onReceive)
|
||||
this.receiver.start()
|
||||
|
||||
abort?.signal.addEventListener("abort", () => {
|
||||
signal.addEventListener("abort", () => {
|
||||
reject(undefined)
|
||||
cleanup()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user