Rework logger, improve plaintext caching
This commit is contained in:
@@ -2,7 +2,6 @@ export * from "./app.js"
|
||||
export * from "./policy.js"
|
||||
export * from "./user.js"
|
||||
export * from "./session.js"
|
||||
export * from "./logging.js"
|
||||
export * from "./createApp.js"
|
||||
export * from "./plugins/base.js"
|
||||
export * from "./plugins/network.js"
|
||||
@@ -12,6 +11,7 @@ export * from "./plugins/relays.js"
|
||||
export * from "./plugins/relayStats.js"
|
||||
export * from "./plugins/relayLists.js"
|
||||
export * from "./plugins/blockedRelayLists.js"
|
||||
export * from "./plugins/logger.js"
|
||||
export * from "./plugins/plaintext.js"
|
||||
export * from "./plugins/profiles.js"
|
||||
export * from "./plugins/follows.js"
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import {randomId} from "@welshman/lib"
|
||||
import {WrappedSigner} from "@welshman/signer"
|
||||
import type {ISigner} from "@welshman/signer"
|
||||
|
||||
/**
|
||||
* A structured, extensible log event. The built-in `signer` variant tracks each
|
||||
* signer operation (sign/encrypt/decrypt/getPubkey); the open variant lets
|
||||
* callers emit their own event types — it's not just a string.
|
||||
*/
|
||||
export type LogMessage =
|
||||
| {
|
||||
type: "signer"
|
||||
id: string
|
||||
method: string
|
||||
status: "pending" | "success" | "failure"
|
||||
error?: unknown
|
||||
at: number
|
||||
}
|
||||
| {type: string; at: number; [key: string]: unknown}
|
||||
|
||||
/**
|
||||
* An `ISigner` wrapper that emits a structured `LogMessage` (as a "message"
|
||||
* event on itself) for every operation it performs. `User.fromSigner` wraps
|
||||
* signers in this so they're observable; subscribe via `makeAppPolicyLogger`.
|
||||
*/
|
||||
export class LoggingSigner extends WrappedSigner {
|
||||
constructor(signer: ISigner) {
|
||||
super(signer, async (method, thunk) => {
|
||||
const id = randomId()
|
||||
|
||||
this.emit("message", {type: "signer", id, method, status: "pending", at: Date.now()})
|
||||
|
||||
try {
|
||||
const result = await thunk()
|
||||
|
||||
this.emit("message", {type: "signer", id, method, status: "success", at: Date.now()})
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
this.emit("message", {type: "signer", id, method, status: "failure", error, at: Date.now()})
|
||||
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import {writable} from "svelte/store"
|
||||
import {randomId} from "@welshman/lib"
|
||||
import {projection} from "./base.js"
|
||||
import type {IApp} from "../app.js"
|
||||
|
||||
export type LogMessage = {
|
||||
source: string
|
||||
id: string
|
||||
at: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* A logger which stores messages durably for inspection. Subscribe to `messages`
|
||||
* (a projection) to read the log; append with `log(source, {...})`.
|
||||
*/
|
||||
export class Logger {
|
||||
protected store = writable<LogMessage[]>([])
|
||||
messages = projection(this.store)
|
||||
|
||||
constructor(protected readonly app: IApp) {}
|
||||
|
||||
log(
|
||||
source: string,
|
||||
{id = randomId(), at = Date.now(), ...message}: {id?: string; at?: number; [key: string]: unknown},
|
||||
) {
|
||||
this.store.update($messages => $messages.concat({source, id, at, ...message}).slice(-1000))
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,20 @@
|
||||
import {nthEq} from "@welshman/lib"
|
||||
import {MUTES} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import type {ISigner} from "@welshman/signer"
|
||||
import {MuteList, MuteListBuilder} from "@welshman/domain"
|
||||
import {DerivedPlugin} from "./base.js"
|
||||
import type {IApp} from "../app.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import {Plaintext} from "./plaintext.js"
|
||||
import {User} from "../user.js"
|
||||
|
||||
/**
|
||||
* A signer that decrypts via the app's plaintext cache (keyed by event), falling
|
||||
* back to the real signer. Lets `MuteList.fromEvent(event, signer)` reuse cached
|
||||
* decryptions instead of re-decrypting. Returns undefined when there's no user,
|
||||
* so the reader falls back to public-only.
|
||||
*/
|
||||
const makeCachedSigner = (app: IApp, event: TrustedEvent): ISigner | undefined => {
|
||||
const user = app.user
|
||||
|
||||
if (!user) return undefined
|
||||
|
||||
const {signer} = user
|
||||
const decryptVia =
|
||||
(fallback: (pubkey: string, message: string) => Promise<string>) =>
|
||||
async (pubkey: string, message: string) =>
|
||||
(await app.use(Plaintext).ensure(event)) ?? fallback(pubkey, message)
|
||||
|
||||
return {
|
||||
sign: (event, options) => signer.sign(event, options),
|
||||
getPubkey: () => signer.getPubkey(),
|
||||
nip04: {
|
||||
encrypt: (pubkey, message) => signer.nip04.encrypt(pubkey, message),
|
||||
decrypt: decryptVia((pubkey, message) => signer.nip04.decrypt(pubkey, message)),
|
||||
},
|
||||
nip44: {
|
||||
encrypt: (pubkey, message) => signer.nip44.encrypt(pubkey, message),
|
||||
decrypt: decryptVia((pubkey, message) => signer.nip44.decrypt(pubkey, message)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kind-10000 mute lists, keyed by pubkey. Mute lists carry private entries in
|
||||
* encrypted content, decoded through the plaintext cache (via a cache-backed
|
||||
* signer passed to the reader).
|
||||
* Kind-10000 mute lists, keyed by pubkey.
|
||||
*/
|
||||
export class MuteLists extends DerivedPlugin<MuteList> {
|
||||
constructor(app: IApp) {
|
||||
super(app, {
|
||||
filters: [{kinds: [MUTES]}],
|
||||
eventToItem: event => MuteList.fromEvent(event, makeCachedSigner(app, event)),
|
||||
eventToItem: MuteList.factory(app.user?.signer),
|
||||
getKey: mute => mute.author(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
import {decrypt} from "@welshman/signer"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {MapPlugin} from "./base.js"
|
||||
|
||||
/**
|
||||
* A cache of decrypted event content, keyed by event id.
|
||||
* A cache of decrypted content, keyed by the ciphertext. Decryption itself is
|
||||
* supplied by the caller (the signer's underlying decrypt), so the cache stays
|
||||
* independent of which signer produced the plaintext — and `appPolicyCacheDecrypt`
|
||||
* can layer it onto `user.signer` without recursing back into the wrapped signer.
|
||||
*/
|
||||
export class Plaintext extends MapPlugin<string> {
|
||||
ensure = async (event: TrustedEvent): Promise<Maybe<string>> => {
|
||||
if (this.app.user?.pubkey !== event.pubkey) return
|
||||
ensure = async (ciphertext: string, decrypt: () => Promise<string>): Promise<string> => {
|
||||
let result = this.get(ciphertext)
|
||||
|
||||
let result = this.get(event.id)
|
||||
if (event.content && result === undefined) {
|
||||
try {
|
||||
result = await decrypt(this.app.user.signer, event.pubkey, event.content)
|
||||
this.set(event.id, result)
|
||||
} catch (e: any) {
|
||||
if (!String(e).match(/invalid base64/)) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
if (result === undefined) {
|
||||
result = await decrypt()
|
||||
|
||||
this.set(ciphertext, result)
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
+43
-13
@@ -8,8 +8,8 @@ import type {IApp} from "./app.js"
|
||||
import {RelayStats} from "./plugins/relayStats.js"
|
||||
import {Wraps} from "./plugins/wraps.js"
|
||||
import {BlockedRelayLists} from "./plugins/blockedRelayLists.js"
|
||||
import {LoggingSigner} from "./logging.js"
|
||||
import type {LogMessage} from "./logging.js"
|
||||
import {Plaintext} from "./plugins/plaintext.js"
|
||||
import {Logger} from "./plugins/logger.js"
|
||||
|
||||
/**
|
||||
* An app policy is a side effect applied once per app at construction,
|
||||
@@ -121,25 +121,55 @@ export const appPolicyWraps: AppPolicy = app => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards "message" events from the user's signer to `onMessage`. Opt-in —
|
||||
* add `makeAppPolicyLogger(handler)` to an app's `policies`.
|
||||
* Wraps user.signer in a WrappedSigner which checks the plaintext cache before
|
||||
* attempting to decrypt something.
|
||||
*/
|
||||
export const makeAppPolicyLogger =
|
||||
(onMessage: (message: LogMessage) => void): AppPolicy =>
|
||||
app => {
|
||||
const unsubscribers: Unsubscriber[] = []
|
||||
const signer = app.user?.signer
|
||||
export const appPolicyCacheDecrypt: AppPolicy = app => {
|
||||
if (!app.user) return noop
|
||||
|
||||
if (signer instanceof LoggingSigner) {
|
||||
unsubscribers.push(on(signer, "message", onMessage))
|
||||
return app.user.wrapSigner((method, thunk, args) => {
|
||||
if (method === "nip04.decrypt" || method === "nip44.decrypt") {
|
||||
const ciphertext = args[1] as string
|
||||
|
||||
return app.use(Plaintext).ensure(ciphertext, thunk as () => Promise<string>) as ReturnType<
|
||||
typeof thunk
|
||||
>
|
||||
}
|
||||
|
||||
return () => unsubscribers.forEach(call)
|
||||
}
|
||||
return thunk()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps user.signer in a WrappedSigner which logs sign requests to the app logger.
|
||||
*/
|
||||
export const appPolicyLogSignerMethods: AppPolicy = app => {
|
||||
if (!app.user) return noop
|
||||
|
||||
const logger = app.use(Logger)
|
||||
|
||||
return app.user.wrapSigner(async (method, thunk) => {
|
||||
logger.log("signer", {method, status: "pending"})
|
||||
|
||||
try {
|
||||
const result = await thunk()
|
||||
|
||||
logger.log("signer", {method, status: "success"})
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
logger.log("signer", {method, status: "failure", error})
|
||||
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const defaultAppPolicies: AppPolicy[] = [
|
||||
appPolicyIngest,
|
||||
appPolicyRelayStats,
|
||||
appPolicyWraps,
|
||||
appPolicyCacheDecrypt,
|
||||
appPolicyLogSignerMethods,
|
||||
appPolicyAuthUnlessBlocked,
|
||||
]
|
||||
|
||||
+19
-10
@@ -1,6 +1,6 @@
|
||||
import type {StampedEvent} from "@welshman/util"
|
||||
import type {ISigner} from "@welshman/signer"
|
||||
import {LoggingSigner} from "./logging.js"
|
||||
import {WrappedSigner} from "@welshman/signer"
|
||||
import type {ISigner, SignerMethodWrapper} from "@welshman/signer"
|
||||
import {getSignerFromSession} from "./session.js"
|
||||
import type {Session} from "./session.js"
|
||||
import type {IApp} from "./app.js"
|
||||
@@ -9,18 +9,18 @@ import type {IApp} from "./app.js"
|
||||
* A single identity: a pubkey plus the signer that proves it. An `App` is
|
||||
* centered on (at most) one `User`, since the data a user can access depends
|
||||
* entirely on who they are.
|
||||
*
|
||||
* `signer` is mutable so app policies can layer behavior onto it after
|
||||
* construction via `wrapSigner` — e.g. `appPolicyCacheDecrypt` and
|
||||
* `appPolicyLogSignerMethods`.
|
||||
*/
|
||||
export class User {
|
||||
constructor(
|
||||
readonly pubkey: string,
|
||||
readonly signer: ISigner,
|
||||
public signer: ISigner,
|
||||
) {}
|
||||
|
||||
static async fromSigner(signer: ISigner) {
|
||||
if (!(signer instanceof LoggingSigner)) {
|
||||
signer = new LoggingSigner(signer)
|
||||
}
|
||||
|
||||
const pubkey = await signer.getPubkey()
|
||||
|
||||
return new User(pubkey, signer)
|
||||
@@ -28,9 +28,8 @@ export class User {
|
||||
|
||||
/**
|
||||
* Reconstruct a signing user from a persisted session, using the registered
|
||||
* session handlers to find the one for the session's method. The signer is
|
||||
* wrapped in a `LoggingSigner` (observe it with `makeAppPolicyLogger`) and the
|
||||
* pubkey is derived from it. Returns undefined when no handler is registered
|
||||
* session handlers to find the one for the session's method. The pubkey is
|
||||
* derived from the signer. Returns undefined when no handler is registered
|
||||
* for the session's method.
|
||||
*/
|
||||
static async fromSession(session: Session): Promise<User | undefined> {
|
||||
@@ -54,4 +53,14 @@ export class User {
|
||||
sign = (event: StampedEvent) => this.signer.sign(event)
|
||||
|
||||
nip44EncryptToSelf = (payload: string) => this.signer.nip44.encrypt(this.pubkey, payload)
|
||||
|
||||
wrapSigner = (wrap: SignerMethodWrapper) => {
|
||||
const original = this.signer
|
||||
|
||||
this.signer = new WrappedSigner(original, wrap)
|
||||
|
||||
return () => {
|
||||
this.signer = original
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user