rework client auth
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import type {Unsubscriber} from "svelte/store"
|
||||
import {call} from "@welshman/lib"
|
||||
import {Pool, Socket, Tracker, Repository, WrapManager, defaultSocketPolicies} from "@welshman/net"
|
||||
import type {NetContext, AdapterFactory, SocketPolicy} from "@welshman/net"
|
||||
import {Pool, Tracker, Repository, WrapManager} from "@welshman/net"
|
||||
import type {NetContext, AdapterFactory} from "@welshman/net"
|
||||
import type {User} from "./user.js"
|
||||
import type {ClientPolicy} from "./policies.js"
|
||||
|
||||
@@ -16,7 +16,6 @@ export type ClientOptions = {
|
||||
user?: User
|
||||
config?: ClientConfig
|
||||
getAdapter?: AdapterFactory
|
||||
socketPolicies?: SocketPolicy[]
|
||||
policies?: ClientPolicy[]
|
||||
}
|
||||
|
||||
@@ -46,26 +45,13 @@ export class Client implements IClient {
|
||||
repository: Repository
|
||||
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 policyCleanups: Unsubscriber[] = []
|
||||
private unsubscribers: Unsubscriber[] = []
|
||||
|
||||
constructor(options: ClientOptions = {}) {
|
||||
this.user = options.user
|
||||
this.config = options.config ?? {}
|
||||
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.pool = new Pool()
|
||||
this.tracker = new Tracker()
|
||||
this.repository = new Repository()
|
||||
this.wrapManager = new WrapManager({
|
||||
@@ -78,9 +64,9 @@ export class Client implements IClient {
|
||||
getAdapter: options.getAdapter,
|
||||
}
|
||||
|
||||
// Apply policies last, once the primitives and `use` registry exist. They
|
||||
// own all side effects; their cleanups run on `cleanup()`.
|
||||
this.policyCleanups = (options.policies ?? []).map(policy => policy(this))
|
||||
for (const policy of options.policies ?? []) {
|
||||
this.unsubscribers.push(policy(this))
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the per-client singleton of a data module, constructing it on first
|
||||
@@ -97,7 +83,7 @@ export class Client implements IClient {
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.policyCleanups.forEach(call)
|
||||
this.unsubscribers.forEach(call)
|
||||
this.pool.clear()
|
||||
this.tracker.clear()
|
||||
this.repository.clear()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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 type {TrustedEvent} from "@welshman/util"
|
||||
import {SocketEvent, isRelayEvent} from "@welshman/net"
|
||||
import type {RelayMessage} from "@welshman/net"
|
||||
import {SocketEvent, isRelayEvent, makeSocketPolicyAuth} from "@welshman/net"
|
||||
import type {RelayMessage, Socket} from "@welshman/net"
|
||||
import type {IClient} from "./client.js"
|
||||
import {RelayStats} from "./relayStats.js"
|
||||
import {GiftWraps} from "./giftWraps.js"
|
||||
@@ -17,6 +17,41 @@ import {GiftWraps} from "./giftWraps.js"
|
||||
*/
|
||||
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
|
||||
* net layer doesn't do this for us, and it's how all the repository-backed
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "@welshman/signer"
|
||||
import type {ISigner} from "@welshman/signer"
|
||||
import {User} from "./user.js"
|
||||
import type {UserOptions} from "./user.js"
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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)
|
||||
|
||||
return signer && typeof session.pubkey === "string"
|
||||
? new User(session.pubkey, signer, options)
|
||||
? new User(session.pubkey, signer)
|
||||
: undefined
|
||||
}
|
||||
|
||||
// Login helpers — each returns a User to build a client/app with
|
||||
|
||||
export const loginWithNip01 = (secret: string, options?: UserOptions) =>
|
||||
userFromSession(makeNip01Session(secret), options)
|
||||
export const loginWithNip01 = (secret: string) => userFromSession(makeNip01Session(secret))
|
||||
|
||||
export const loginWithNip07 = (pubkey: string, options?: UserOptions) =>
|
||||
userFromSession(makeNip07Session(pubkey), options)
|
||||
export const loginWithNip07 = (pubkey: string) => userFromSession(makeNip07Session(pubkey))
|
||||
|
||||
export const loginWithNip46 = (
|
||||
pubkey: string,
|
||||
clientSecret: string,
|
||||
signerPubkey: string,
|
||||
relays: string[],
|
||||
options?: UserOptions,
|
||||
) => userFromSession(makeNip46Session(pubkey, clientSecret, signerPubkey, relays), options)
|
||||
) => userFromSession(makeNip46Session(pubkey, clientSecret, signerPubkey, relays))
|
||||
|
||||
export const loginWithNip55 = (pubkey: string, signer: string, options?: UserOptions) =>
|
||||
userFromSession(makeNip55Session(pubkey, signer), options)
|
||||
export const loginWithNip55 = (pubkey: string, signer: string) =>
|
||||
userFromSession(makeNip55Session(pubkey, signer))
|
||||
|
||||
export const loginWithPomade = (
|
||||
pubkey: string,
|
||||
email: string,
|
||||
clientOptions: PomadeClientOptions,
|
||||
options?: UserOptions,
|
||||
) => userFromSession(makePomadeSession(pubkey, email, clientOptions), options)
|
||||
) => userFromSession(makePomadeSession(pubkey, email, clientOptions))
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import {makeSocketPolicyAuth} from "@welshman/net"
|
||||
import type {Socket} from "@welshman/net"
|
||||
import type {StampedEvent} from "@welshman/util"
|
||||
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
|
||||
* centered on (at most) one `User`, since the data a user can access depends
|
||||
@@ -16,21 +10,14 @@ export class User {
|
||||
constructor(
|
||||
readonly pubkey: string,
|
||||
readonly signer: ISigner,
|
||||
readonly options: UserOptions = {},
|
||||
) {}
|
||||
|
||||
static async fromSigner(signer: ISigner, options: UserOptions = {}) {
|
||||
static async fromSigner(signer: ISigner) {
|
||||
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)
|
||||
|
||||
nip44EncryptToSelf = (payload: string) => this.signer.nip44.encrypt(this.pubkey, payload)
|
||||
|
||||
Reference in New Issue
Block a user