rework client auth
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import type {Unsubscriber} from "svelte/store"
|
import type {Unsubscriber} from "svelte/store"
|
||||||
import {call} from "@welshman/lib"
|
import {call} from "@welshman/lib"
|
||||||
import {Pool, Socket, Tracker, Repository, WrapManager, defaultSocketPolicies} from "@welshman/net"
|
import {Pool, Tracker, Repository, WrapManager} from "@welshman/net"
|
||||||
import type {NetContext, AdapterFactory, SocketPolicy} from "@welshman/net"
|
import type {NetContext, AdapterFactory} from "@welshman/net"
|
||||||
import type {User} from "./user.js"
|
import type {User} from "./user.js"
|
||||||
import type {ClientPolicy} from "./policies.js"
|
import type {ClientPolicy} from "./policies.js"
|
||||||
|
|
||||||
@@ -16,7 +16,6 @@ export type ClientOptions = {
|
|||||||
user?: User
|
user?: User
|
||||||
config?: ClientConfig
|
config?: ClientConfig
|
||||||
getAdapter?: AdapterFactory
|
getAdapter?: AdapterFactory
|
||||||
socketPolicies?: SocketPolicy[]
|
|
||||||
policies?: ClientPolicy[]
|
policies?: ClientPolicy[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,26 +45,13 @@ export class Client implements IClient {
|
|||||||
repository: Repository
|
repository: Repository
|
||||||
wrapManager: WrapManager
|
wrapManager: WrapManager
|
||||||
|
|
||||||
// Per-client singletons of data modules, keyed by constructor. Owned by the
|
|
||||||
// client (so it's GC'd with the client — no WeakMap needed), this is what
|
|
||||||
// `use` memoizes against.
|
|
||||||
private singletons = new Map<Function, unknown>()
|
private singletons = new Map<Function, unknown>()
|
||||||
private policyCleanups: Unsubscriber[] = []
|
private unsubscribers: Unsubscriber[] = []
|
||||||
|
|
||||||
constructor(options: ClientOptions = {}) {
|
constructor(options: ClientOptions = {}) {
|
||||||
this.user = options.user
|
this.user = options.user
|
||||||
this.config = options.config ?? {}
|
this.config = options.config ?? {}
|
||||||
this.pool = new Pool({
|
this.pool = new Pool()
|
||||||
makeSocket: (url: string) => {
|
|
||||||
let socketPolicies = options.socketPolicies ?? defaultSocketPolicies
|
|
||||||
|
|
||||||
if (this.user) {
|
|
||||||
socketPolicies = [...socketPolicies, this.user.makeSocketPolicyAuth()]
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Socket(url, socketPolicies)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
this.tracker = new Tracker()
|
this.tracker = new Tracker()
|
||||||
this.repository = new Repository()
|
this.repository = new Repository()
|
||||||
this.wrapManager = new WrapManager({
|
this.wrapManager = new WrapManager({
|
||||||
@@ -78,9 +64,9 @@ export class Client implements IClient {
|
|||||||
getAdapter: options.getAdapter,
|
getAdapter: options.getAdapter,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply policies last, once the primitives and `use` registry exist. They
|
for (const policy of options.policies ?? []) {
|
||||||
// own all side effects; their cleanups run on `cleanup()`.
|
this.unsubscribers.push(policy(this))
|
||||||
this.policyCleanups = (options.policies ?? []).map(policy => policy(this))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the per-client singleton of a data module, constructing it on first
|
// Resolve the per-client singleton of a data module, constructing it on first
|
||||||
@@ -97,7 +83,7 @@ export class Client implements IClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
this.policyCleanups.forEach(call)
|
this.unsubscribers.forEach(call)
|
||||||
this.pool.clear()
|
this.pool.clear()
|
||||||
this.tracker.clear()
|
this.tracker.clear()
|
||||||
this.repository.clear()
|
this.repository.clear()
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type {Unsubscriber} from "svelte/store"
|
import type {Unsubscriber} from "svelte/store"
|
||||||
import {on} from "@welshman/lib"
|
import {on, noop, always} from "@welshman/lib"
|
||||||
import {WRAP, isDVMKind, isEphemeralKind, verifyEvent} from "@welshman/util"
|
import {WRAP, isDVMKind, isEphemeralKind, verifyEvent} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {SocketEvent, isRelayEvent} from "@welshman/net"
|
import {SocketEvent, isRelayEvent, makeSocketPolicyAuth} from "@welshman/net"
|
||||||
import type {RelayMessage} from "@welshman/net"
|
import type {RelayMessage, Socket} from "@welshman/net"
|
||||||
import type {IClient} from "./client.js"
|
import type {IClient} from "./client.js"
|
||||||
import {RelayStats} from "./relayStats.js"
|
import {RelayStats} from "./relayStats.js"
|
||||||
import {GiftWraps} from "./giftWraps.js"
|
import {GiftWraps} from "./giftWraps.js"
|
||||||
@@ -17,6 +17,41 @@ import {GiftWraps} from "./giftWraps.js"
|
|||||||
*/
|
*/
|
||||||
export type ClientPolicy = (client: IClient) => Unsubscriber
|
export type ClientPolicy = (client: IClient) => Unsubscriber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a client policy that authenticates the client's sockets (NIP-42) with
|
||||||
|
* the user's signer. It appends an auth socket policy to the pool's
|
||||||
|
* `socketPolicies`, so every socket the pool creates answers AUTH challenges
|
||||||
|
* according to `shouldAuth`; the policy is spliced back out on cleanup. No-op
|
||||||
|
* when the client has no user.
|
||||||
|
*
|
||||||
|
* Use the `clientPolicyAuthAlways` / `clientPolicyAuthNever` presets below, or
|
||||||
|
* call this with a custom predicate.
|
||||||
|
*/
|
||||||
|
export const makeClientPolicyAuth =
|
||||||
|
(shouldAuth: (socket: Socket) => boolean): ClientPolicy =>
|
||||||
|
client => {
|
||||||
|
if (!client.user) {
|
||||||
|
return noop
|
||||||
|
}
|
||||||
|
|
||||||
|
const {sign} = client.user.signer
|
||||||
|
const policy = makeSocketPolicyAuth({sign, shouldAuth})
|
||||||
|
|
||||||
|
client.pool.socketPolicies.push(policy)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const index = client.pool.socketPolicies.indexOf(policy)
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
client.pool.socketPolicies.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clientPolicyAuthNever = makeClientPolicyAuth(always(false))
|
||||||
|
|
||||||
|
export const clientPolicyAuthAlways = makeClientPolicyAuth(always(true))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ingests every event received on any socket into the client's repository. The
|
* Ingests every event received on any socket into the client's repository. The
|
||||||
* net layer doesn't do this for us, and it's how all the repository-backed
|
* net layer doesn't do this for us, and it's how all the repository-backed
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
} from "@welshman/signer"
|
} from "@welshman/signer"
|
||||||
import type {ISigner} from "@welshman/signer"
|
import type {ISigner} from "@welshman/signer"
|
||||||
import {User} from "./user.js"
|
import {User} from "./user.js"
|
||||||
import type {UserOptions} from "./user.js"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Session descriptors and the signer construction that turns them into a
|
* Session descriptors and the signer construction that turns them into a
|
||||||
@@ -214,36 +213,32 @@ export const getSigner = (session?: Session): ISigner | undefined => {
|
|||||||
* for sessions that can't sign (e.g. read-only Pubkey or Anonymous). Pass the
|
* for sessions that can't sign (e.g. read-only Pubkey or Anonymous). Pass the
|
||||||
* result to `new Client({user})` / `createApp({user})`.
|
* result to `new Client({user})` / `createApp({user})`.
|
||||||
*/
|
*/
|
||||||
export const userFromSession = (session: Session, options: UserOptions = {}): User | undefined => {
|
export const userFromSession = (session: Session): User | undefined => {
|
||||||
const signer = getSigner(session)
|
const signer = getSigner(session)
|
||||||
|
|
||||||
return signer && typeof session.pubkey === "string"
|
return signer && typeof session.pubkey === "string"
|
||||||
? new User(session.pubkey, signer, options)
|
? new User(session.pubkey, signer)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login helpers — each returns a User to build a client/app with
|
// Login helpers — each returns a User to build a client/app with
|
||||||
|
|
||||||
export const loginWithNip01 = (secret: string, options?: UserOptions) =>
|
export const loginWithNip01 = (secret: string) => userFromSession(makeNip01Session(secret))
|
||||||
userFromSession(makeNip01Session(secret), options)
|
|
||||||
|
|
||||||
export const loginWithNip07 = (pubkey: string, options?: UserOptions) =>
|
export const loginWithNip07 = (pubkey: string) => userFromSession(makeNip07Session(pubkey))
|
||||||
userFromSession(makeNip07Session(pubkey), options)
|
|
||||||
|
|
||||||
export const loginWithNip46 = (
|
export const loginWithNip46 = (
|
||||||
pubkey: string,
|
pubkey: string,
|
||||||
clientSecret: string,
|
clientSecret: string,
|
||||||
signerPubkey: string,
|
signerPubkey: string,
|
||||||
relays: string[],
|
relays: string[],
|
||||||
options?: UserOptions,
|
) => userFromSession(makeNip46Session(pubkey, clientSecret, signerPubkey, relays))
|
||||||
) => userFromSession(makeNip46Session(pubkey, clientSecret, signerPubkey, relays), options)
|
|
||||||
|
|
||||||
export const loginWithNip55 = (pubkey: string, signer: string, options?: UserOptions) =>
|
export const loginWithNip55 = (pubkey: string, signer: string) =>
|
||||||
userFromSession(makeNip55Session(pubkey, signer), options)
|
userFromSession(makeNip55Session(pubkey, signer))
|
||||||
|
|
||||||
export const loginWithPomade = (
|
export const loginWithPomade = (
|
||||||
pubkey: string,
|
pubkey: string,
|
||||||
email: string,
|
email: string,
|
||||||
clientOptions: PomadeClientOptions,
|
clientOptions: PomadeClientOptions,
|
||||||
options?: UserOptions,
|
) => userFromSession(makePomadeSession(pubkey, email, clientOptions))
|
||||||
) => userFromSession(makePomadeSession(pubkey, email, clientOptions), options)
|
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import {makeSocketPolicyAuth} from "@welshman/net"
|
|
||||||
import type {Socket} from "@welshman/net"
|
|
||||||
import type {StampedEvent} from "@welshman/util"
|
import type {StampedEvent} from "@welshman/util"
|
||||||
import type {ISigner} from "@welshman/signer"
|
import type {ISigner} from "@welshman/signer"
|
||||||
|
|
||||||
export type UserOptions = {
|
|
||||||
shouldAuth?: (socket: Socket) => boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A single identity: a pubkey plus the signer that proves it. A `Client` is
|
* A single identity: a pubkey plus the signer that proves it. A `Client` is
|
||||||
* centered on (at most) one `User`, since the data a user can access depends
|
* centered on (at most) one `User`, since the data a user can access depends
|
||||||
@@ -16,21 +10,14 @@ export class User {
|
|||||||
constructor(
|
constructor(
|
||||||
readonly pubkey: string,
|
readonly pubkey: string,
|
||||||
readonly signer: ISigner,
|
readonly signer: ISigner,
|
||||||
readonly options: UserOptions = {},
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
static async fromSigner(signer: ISigner, options: UserOptions = {}) {
|
static async fromSigner(signer: ISigner) {
|
||||||
const pubkey = await signer.getPubkey()
|
const pubkey = await signer.getPubkey()
|
||||||
|
|
||||||
return new User(pubkey, signer, options)
|
return new User(pubkey, signer)
|
||||||
}
|
}
|
||||||
|
|
||||||
makeSocketPolicyAuth = () =>
|
|
||||||
makeSocketPolicyAuth({
|
|
||||||
sign: this.signer.sign,
|
|
||||||
shouldAuth: this.options.shouldAuth,
|
|
||||||
})
|
|
||||||
|
|
||||||
sign = (event: StampedEvent) => this.signer.sign(event)
|
sign = (event: StampedEvent) => this.signer.sign(event)
|
||||||
|
|
||||||
nip44EncryptToSelf = (payload: string) => this.signer.nip44.encrypt(this.pubkey, payload)
|
nip44EncryptToSelf = (payload: string) => this.signer.nip44.encrypt(this.pubkey, payload)
|
||||||
|
|||||||
@@ -5,28 +5,15 @@ import {defaultSocketPolicies} from "./policy.js"
|
|||||||
|
|
||||||
export type PoolSubscription = (socket: Socket) => void
|
export type PoolSubscription = (socket: Socket) => void
|
||||||
|
|
||||||
export type PoolOptions = {
|
|
||||||
makeSocket?: (url: string) => Socket
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Pool {
|
export class Pool {
|
||||||
|
socketPolicies = [...defaultSocketPolicies]
|
||||||
_data = new Map<string, Socket>()
|
_data = new Map<string, Socket>()
|
||||||
_subs: PoolSubscription[] = []
|
_subs: PoolSubscription[] = []
|
||||||
|
|
||||||
constructor(readonly options: PoolOptions = {}) {}
|
|
||||||
|
|
||||||
has(url: string) {
|
has(url: string) {
|
||||||
return this._data.has(normalizeRelayUrl(url))
|
return this._data.has(normalizeRelayUrl(url))
|
||||||
}
|
}
|
||||||
|
|
||||||
makeSocket(url: string) {
|
|
||||||
if (this.options.makeSocket) {
|
|
||||||
return this.options.makeSocket(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Socket(url, defaultSocketPolicies)
|
|
||||||
}
|
|
||||||
|
|
||||||
get(_url: string): Socket {
|
get(_url: string): Socket {
|
||||||
const url = normalizeRelayUrl(_url)
|
const url = normalizeRelayUrl(_url)
|
||||||
const socket = this._data.get(url)
|
const socket = this._data.get(url)
|
||||||
@@ -35,7 +22,7 @@ export class Pool {
|
|||||||
return socket
|
return socket
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSocket = this.makeSocket(url)
|
const newSocket = new Socket(url, this.socketPolicies)
|
||||||
|
|
||||||
this._data.set(url, newSocket)
|
this._data.set(url, newSocket)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user