applesauce-accounts 0.0.0-next-20250124230519 → 0.0.0-next-20250125183855
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/account.d.ts +17 -5
- package/dist/account.js +110 -20
- package/dist/account.test.d.ts +1 -0
- package/dist/account.test.js +59 -0
- package/dist/manager.d.ts +6 -2
- package/dist/manager.js +27 -17
- package/dist/types.d.ts +1 -0
- package/package.json +2 -2
package/dist/account.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { Nip07Interface } from "applesauce-signer";
|
|
2
|
+
import { BehaviorSubject } from "rxjs";
|
|
3
|
+
import { NostrEvent } from "nostr-tools";
|
|
2
4
|
import { EventTemplate, IAccount, SerializedAccount } from "./types.js";
|
|
3
5
|
export declare class SignerMismatchError extends Error {
|
|
4
6
|
}
|
|
5
|
-
export declare class AccountLockedError extends Error {
|
|
6
|
-
}
|
|
7
7
|
export declare class BaseAccount<Signer extends Nip07Interface, SignerData, Metadata extends unknown> implements IAccount<Signer, SignerData, Metadata> {
|
|
8
8
|
pubkey: string;
|
|
9
9
|
signer: Signer;
|
|
10
10
|
id: string;
|
|
11
|
-
|
|
11
|
+
/** Disable request queueing */
|
|
12
|
+
disableQueue?: boolean;
|
|
13
|
+
metadata$: BehaviorSubject<Metadata | undefined>;
|
|
14
|
+
get metadata(): Metadata | undefined;
|
|
15
|
+
set metadata(metadata: Metadata);
|
|
12
16
|
nip04?: {
|
|
13
17
|
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
|
|
14
18
|
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
|
@@ -20,7 +24,15 @@ export declare class BaseAccount<Signer extends Nip07Interface, SignerData, Meta
|
|
|
20
24
|
constructor(pubkey: string, signer: Signer);
|
|
21
25
|
toJSON(): SerializedAccount<SignerData, Metadata>;
|
|
22
26
|
/** Gets the pubkey from the signer */
|
|
23
|
-
getPublicKey(): Promise<string>;
|
|
27
|
+
getPublicKey(): string | Promise<string>;
|
|
24
28
|
/** sign the event and make sure its signed with the correct pubkey */
|
|
25
|
-
signEvent(template: EventTemplate): Promise<
|
|
29
|
+
signEvent(template: EventTemplate): Promise<NostrEvent> | NostrEvent;
|
|
30
|
+
/** Aborts all pending requests in the queue */
|
|
31
|
+
abortQueue(reason: Error): void;
|
|
32
|
+
/** internal queue */
|
|
33
|
+
protected queueLength: number;
|
|
34
|
+
protected lock: Promise<any> | null;
|
|
35
|
+
protected abort: AbortController | null;
|
|
36
|
+
protected reduceQueue(): void;
|
|
37
|
+
protected waitForLock<T>(fn: () => Promise<T> | T): Promise<T> | T;
|
|
26
38
|
}
|
package/dist/account.js
CHANGED
|
@@ -1,14 +1,41 @@
|
|
|
1
1
|
import { nanoid } from "nanoid";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { BehaviorSubject } from "rxjs";
|
|
3
|
+
function wrapInSignal(promise, signal) {
|
|
4
|
+
return new Promise((res, rej) => {
|
|
5
|
+
signal.throwIfAborted();
|
|
6
|
+
let done = false;
|
|
7
|
+
// reject promise if abort signal is triggered
|
|
8
|
+
signal.addEventListener("abort", () => {
|
|
9
|
+
if (!done)
|
|
10
|
+
rej(signal.reason || undefined);
|
|
11
|
+
done = true;
|
|
12
|
+
});
|
|
13
|
+
return promise.then((v) => {
|
|
14
|
+
if (!done)
|
|
15
|
+
res(v);
|
|
16
|
+
done = true;
|
|
17
|
+
}, (err) => {
|
|
18
|
+
if (!done)
|
|
19
|
+
rej(err);
|
|
20
|
+
done = true;
|
|
21
|
+
});
|
|
22
|
+
});
|
|
4
23
|
}
|
|
5
|
-
export class
|
|
24
|
+
export class SignerMismatchError extends Error {
|
|
6
25
|
}
|
|
7
26
|
export class BaseAccount {
|
|
8
27
|
pubkey;
|
|
9
28
|
signer;
|
|
10
29
|
id = nanoid(8);
|
|
11
|
-
|
|
30
|
+
/** Disable request queueing */
|
|
31
|
+
disableQueue;
|
|
32
|
+
metadata$ = new BehaviorSubject(undefined);
|
|
33
|
+
get metadata() {
|
|
34
|
+
return this.metadata$.value;
|
|
35
|
+
}
|
|
36
|
+
set metadata(metadata) {
|
|
37
|
+
this.metadata$.next(metadata);
|
|
38
|
+
}
|
|
12
39
|
// encryption interfaces
|
|
13
40
|
nip04;
|
|
14
41
|
nip44;
|
|
@@ -19,20 +46,20 @@ export class BaseAccount {
|
|
|
19
46
|
if (this.signer.nip04) {
|
|
20
47
|
this.nip04 = {
|
|
21
48
|
encrypt: (pubkey, plaintext) => {
|
|
22
|
-
return this.signer.nip04.encrypt(pubkey, plaintext);
|
|
49
|
+
return this.waitForLock(() => this.signer.nip04.encrypt(pubkey, plaintext));
|
|
23
50
|
},
|
|
24
51
|
decrypt: (pubkey, plaintext) => {
|
|
25
|
-
return this.signer.nip04.decrypt(pubkey, plaintext);
|
|
52
|
+
return this.waitForLock(() => this.signer.nip04.decrypt(pubkey, plaintext));
|
|
26
53
|
},
|
|
27
54
|
};
|
|
28
55
|
}
|
|
29
56
|
if (this.signer.nip44) {
|
|
30
57
|
this.nip44 = {
|
|
31
58
|
encrypt: (pubkey, plaintext) => {
|
|
32
|
-
return this.signer.nip44.encrypt(pubkey, plaintext);
|
|
59
|
+
return this.waitForLock(() => this.signer.nip44.encrypt(pubkey, plaintext));
|
|
33
60
|
},
|
|
34
61
|
decrypt: (pubkey, plaintext) => {
|
|
35
|
-
return this.signer.nip44.decrypt(pubkey, plaintext);
|
|
62
|
+
return this.waitForLock(() => this.signer.nip44.decrypt(pubkey, plaintext));
|
|
36
63
|
},
|
|
37
64
|
};
|
|
38
65
|
}
|
|
@@ -42,21 +69,84 @@ export class BaseAccount {
|
|
|
42
69
|
throw new Error("Not implemented");
|
|
43
70
|
}
|
|
44
71
|
/** Gets the pubkey from the signer */
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
72
|
+
getPublicKey() {
|
|
73
|
+
const result = this.signer.getPublicKey();
|
|
74
|
+
if (result instanceof Promise)
|
|
75
|
+
return result.then((pubkey) => {
|
|
76
|
+
if (this.pubkey !== pubkey)
|
|
77
|
+
throw new SignerMismatchError("Account signer mismatch");
|
|
78
|
+
return pubkey;
|
|
79
|
+
});
|
|
80
|
+
else {
|
|
81
|
+
if (this.pubkey !== result)
|
|
82
|
+
throw new SignerMismatchError("Account signer mismatch");
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
51
85
|
}
|
|
52
86
|
/** sign the event and make sure its signed with the correct pubkey */
|
|
53
|
-
|
|
54
|
-
// this.checkLocked();
|
|
87
|
+
signEvent(template) {
|
|
55
88
|
if (!Reflect.has(template, "pubkey"))
|
|
56
89
|
Reflect.set(template, "pubkey", this.pubkey);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
90
|
+
return this.waitForLock(() => {
|
|
91
|
+
const result = this.signer.signEvent(template);
|
|
92
|
+
if (result instanceof Promise)
|
|
93
|
+
return result.then((signed) => {
|
|
94
|
+
if (signed.pubkey !== this.pubkey)
|
|
95
|
+
throw new SignerMismatchError("Signer signed with wrong pubkey");
|
|
96
|
+
return signed;
|
|
97
|
+
});
|
|
98
|
+
else {
|
|
99
|
+
if (result.pubkey !== this.pubkey)
|
|
100
|
+
throw new SignerMismatchError("Signer signed with wrong pubkey");
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** Aborts all pending requests in the queue */
|
|
106
|
+
abortQueue(reason) {
|
|
107
|
+
if (this.abort)
|
|
108
|
+
this.abort.abort(reason);
|
|
109
|
+
}
|
|
110
|
+
/** internal queue */
|
|
111
|
+
queueLength = 0;
|
|
112
|
+
lock = null;
|
|
113
|
+
abort = null;
|
|
114
|
+
reduceQueue() {
|
|
115
|
+
// shorten the queue
|
|
116
|
+
this.queueLength--;
|
|
117
|
+
// if this was the last request, remove the lock
|
|
118
|
+
if (this.queueLength === 0) {
|
|
119
|
+
this.lock = null;
|
|
120
|
+
this.abort = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
waitForLock(fn) {
|
|
124
|
+
if (this.disableQueue)
|
|
125
|
+
return fn();
|
|
126
|
+
// if there is already a pending request, wait for it
|
|
127
|
+
if (this.lock && this.abort) {
|
|
128
|
+
// create a new promise that runs after the lock
|
|
129
|
+
const p = wrapInSignal(this.lock.then(() => {
|
|
130
|
+
// if the abort signal is triggered, don't call the signer
|
|
131
|
+
this.abort?.signal.throwIfAborted();
|
|
132
|
+
return fn();
|
|
133
|
+
}), this.abort.signal);
|
|
134
|
+
// set the lock the new promise that ignores errors
|
|
135
|
+
this.lock = p.catch(() => { }).finally(this.reduceQueue.bind(this));
|
|
136
|
+
this.queueLength++;
|
|
137
|
+
return p;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
const result = fn();
|
|
141
|
+
// if the result is async, set the new lock
|
|
142
|
+
if (result instanceof Promise) {
|
|
143
|
+
this.abort = new AbortController();
|
|
144
|
+
const p = wrapInSignal(result, this.abort.signal);
|
|
145
|
+
// set the lock the new promise that ignores errors
|
|
146
|
+
this.lock = p.catch(() => { }).finally(this.reduceQueue.bind(this));
|
|
147
|
+
this.queueLength = 1;
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
61
151
|
}
|
|
62
152
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { BaseAccount } from "./account.js";
|
|
3
|
+
import { SimpleSigner } from "applesauce-signer";
|
|
4
|
+
import { finalizeEvent } from "nostr-tools";
|
|
5
|
+
describe("BaseAccount", () => {
|
|
6
|
+
let signer;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
signer = new SimpleSigner();
|
|
9
|
+
});
|
|
10
|
+
describe("request queue", () => {
|
|
11
|
+
it("should queue signing requests by default", async () => {
|
|
12
|
+
const account = new BaseAccount(await signer.getPublicKey(), signer);
|
|
13
|
+
let resolve = [];
|
|
14
|
+
vi.spyOn(signer, "signEvent").mockImplementation(() => {
|
|
15
|
+
return new Promise((res) => {
|
|
16
|
+
resolve.push(() => res(finalizeEvent({ kind: 1, content: "mock", created_at: 0, tags: [] }, signer.key)));
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
// make two signing requests
|
|
20
|
+
expect(account.signEvent({ kind: 1, content: "first", created_at: 0, tags: [] })).toEqual(expect.any(Promise));
|
|
21
|
+
expect(account.signEvent({ kind: 1, content: "second", created_at: 0, tags: [] })).toEqual(expect.any(Promise));
|
|
22
|
+
expect(signer.signEvent).toHaveBeenCalledOnce();
|
|
23
|
+
expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "first" }));
|
|
24
|
+
// resolve first
|
|
25
|
+
resolve.shift()?.();
|
|
26
|
+
// wait next tick
|
|
27
|
+
await new Promise((res) => setTimeout(res, 0));
|
|
28
|
+
expect(signer.signEvent).toHaveBeenCalledTimes(2);
|
|
29
|
+
expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "second" }));
|
|
30
|
+
// resolve second
|
|
31
|
+
resolve.shift()?.();
|
|
32
|
+
// wait next tick
|
|
33
|
+
await new Promise((res) => setTimeout(res, 0));
|
|
34
|
+
expect(Reflect.get(account, "queueLength")).toBe(0);
|
|
35
|
+
expect(Reflect.get(account, "lock")).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
it("should cancel queue if request throws", () => { });
|
|
38
|
+
it("should not use queueing if its disabled", async () => {
|
|
39
|
+
const account = new BaseAccount(await signer.getPublicKey(), signer);
|
|
40
|
+
account.disableQueue = false;
|
|
41
|
+
let resolve = [];
|
|
42
|
+
vi.spyOn(signer, "signEvent").mockImplementation(() => {
|
|
43
|
+
return new Promise((res) => {
|
|
44
|
+
resolve.push(() => res(finalizeEvent({ kind: 1, content: "mock", created_at: 0, tags: [] }, signer.key)));
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
// make two signing requests
|
|
48
|
+
account.signEvent({ kind: 1, content: "first", created_at: 0, tags: [] });
|
|
49
|
+
account.signEvent({ kind: 1, content: "second", created_at: 0, tags: [] });
|
|
50
|
+
expect(Reflect.get(account, "lock")).toBeNull();
|
|
51
|
+
expect(signer.signEvent).toHaveBeenCalledTimes(2);
|
|
52
|
+
expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "first" }));
|
|
53
|
+
expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "second" }));
|
|
54
|
+
// resolve both
|
|
55
|
+
resolve.shift()?.();
|
|
56
|
+
resolve.shift()?.();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
package/dist/manager.d.ts
CHANGED
|
@@ -2,9 +2,13 @@ import { Nip07Interface } from "applesauce-signer";
|
|
|
2
2
|
import { BehaviorSubject } from "rxjs";
|
|
3
3
|
import { IAccount, IAccountConstructor, SerializedAccount } from "./types.js";
|
|
4
4
|
export declare class AccountManager<Metadata extends unknown = any> {
|
|
5
|
-
active: BehaviorSubject<IAccount<any, any, Metadata> | null>;
|
|
6
|
-
accounts: BehaviorSubject<Record<string, IAccount<any, any, Metadata>>>;
|
|
7
5
|
types: Map<string, IAccountConstructor<any, any, Metadata>>;
|
|
6
|
+
active$: BehaviorSubject<IAccount<any, any, Metadata> | null>;
|
|
7
|
+
get active(): IAccount<any, any, Metadata> | null;
|
|
8
|
+
accounts$: BehaviorSubject<IAccount<any, any, Metadata>[]>;
|
|
9
|
+
get accounts(): IAccount<any, any, Metadata>[];
|
|
10
|
+
/** Disable request queueing for any accounts added to this manager */
|
|
11
|
+
disableQueue?: boolean;
|
|
8
12
|
/** Add account type class */
|
|
9
13
|
registerType<S extends Nip07Interface>(accountType: IAccountConstructor<S, any, Metadata>): void;
|
|
10
14
|
/** Remove account type */
|
package/dist/manager.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { BehaviorSubject } from "rxjs";
|
|
2
2
|
export class AccountManager {
|
|
3
|
-
active = new BehaviorSubject(null);
|
|
4
|
-
accounts = new BehaviorSubject({});
|
|
5
3
|
types = new Map();
|
|
4
|
+
active$ = new BehaviorSubject(null);
|
|
5
|
+
get active() {
|
|
6
|
+
return this.active$.value;
|
|
7
|
+
}
|
|
8
|
+
accounts$ = new BehaviorSubject([]);
|
|
9
|
+
get accounts() {
|
|
10
|
+
return this.accounts$.value;
|
|
11
|
+
}
|
|
12
|
+
/** Disable request queueing for any accounts added to this manager */
|
|
13
|
+
disableQueue;
|
|
6
14
|
// Account type CRUD
|
|
7
15
|
/** Add account type class */
|
|
8
16
|
registerType(accountType) {
|
|
@@ -20,62 +28,64 @@ export class AccountManager {
|
|
|
20
28
|
/** gets an account in the manager */
|
|
21
29
|
getAccount(id) {
|
|
22
30
|
if (typeof id === "string")
|
|
23
|
-
return this.accounts.
|
|
24
|
-
else if (this.accounts
|
|
31
|
+
return this.accounts$.value.find((a) => a.id === id);
|
|
32
|
+
else if (this.accounts$.value.includes(id))
|
|
25
33
|
return id;
|
|
26
34
|
else
|
|
27
35
|
return undefined;
|
|
28
36
|
}
|
|
29
37
|
/** Return the first account for a pubkey */
|
|
30
38
|
getAccountForPubkey(pubkey) {
|
|
31
|
-
return Object.values(this.accounts
|
|
39
|
+
return Object.values(this.accounts$.value).find((account) => account.pubkey === pubkey);
|
|
32
40
|
}
|
|
33
41
|
/** Returns all accounts for a pubkey */
|
|
34
42
|
getAccountsForPubkey(pubkey) {
|
|
35
|
-
return Object.values(this.accounts
|
|
43
|
+
return Object.values(this.accounts$.value).filter((account) => account.pubkey === pubkey);
|
|
36
44
|
}
|
|
37
45
|
/** adds an account to the manager */
|
|
38
46
|
addAccount(account) {
|
|
39
47
|
if (this.getAccount(account.id))
|
|
40
48
|
return;
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
// copy the disableQueue flag only if its set
|
|
50
|
+
if (this.disableQueue !== undefined && account.disableQueue !== undefined) {
|
|
51
|
+
account.disableQueue = this.disableQueue;
|
|
52
|
+
}
|
|
53
|
+
this.accounts$.next({
|
|
54
|
+
...this.accounts$.value,
|
|
43
55
|
[account.id]: account,
|
|
44
56
|
});
|
|
45
57
|
}
|
|
46
58
|
/** Removes an account from the manager */
|
|
47
59
|
removeAccount(account) {
|
|
48
60
|
const id = typeof account === "string" ? account : account.id;
|
|
49
|
-
|
|
50
|
-
delete next[id];
|
|
51
|
-
this.accounts.next(next);
|
|
61
|
+
this.accounts$.next(this.accounts$.value.filter((a) => a.id !== id));
|
|
52
62
|
}
|
|
53
63
|
/** Replaces an account with another */
|
|
54
64
|
replaceAccount(old, account) {
|
|
55
65
|
this.addAccount(account);
|
|
56
66
|
// if the old account was active, switch to the new one
|
|
57
67
|
const id = typeof account === "string" ? account : account.id;
|
|
58
|
-
if (this.active
|
|
68
|
+
if (this.active$.value?.id === id)
|
|
59
69
|
this.setActive(account);
|
|
60
70
|
this.removeAccount(old);
|
|
61
71
|
}
|
|
62
72
|
// Active account methods
|
|
63
73
|
/** Returns the currently active account */
|
|
64
74
|
getActive() {
|
|
65
|
-
return this.active
|
|
75
|
+
return this.active$.value;
|
|
66
76
|
}
|
|
67
77
|
/** Sets the currently active account */
|
|
68
78
|
setActive(id) {
|
|
69
79
|
const account = this.getAccount(id);
|
|
70
80
|
if (!account)
|
|
71
81
|
throw new Error("Cant find account with that ID");
|
|
72
|
-
if (this.active
|
|
73
|
-
this.active
|
|
82
|
+
if (this.active$.value?.id !== account.id) {
|
|
83
|
+
this.active$.next(account);
|
|
74
84
|
}
|
|
75
85
|
}
|
|
76
86
|
/** Clears the currently active account */
|
|
77
87
|
clearActive() {
|
|
78
|
-
this.active
|
|
88
|
+
this.active$.next(null);
|
|
79
89
|
}
|
|
80
90
|
// Metadata CRUD
|
|
81
91
|
/** sets the metadata on an account */
|
|
@@ -95,7 +105,7 @@ export class AccountManager {
|
|
|
95
105
|
// Serialize / Deserialize
|
|
96
106
|
/** Returns an array of serialized accounts */
|
|
97
107
|
toJSON() {
|
|
98
|
-
return Array.from(Object.values(this.accounts)).map((account) => account.toJSON());
|
|
108
|
+
return Array.from(Object.values(this.accounts$)).map((account) => account.toJSON());
|
|
99
109
|
}
|
|
100
110
|
/**
|
|
101
111
|
* Restores all accounts from an array of serialized accounts
|
package/dist/types.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface IAccount<Signer extends Nip07Interface, SignerData, Metadata ex
|
|
|
25
25
|
pubkey: string;
|
|
26
26
|
metadata?: Metadata;
|
|
27
27
|
signer: Signer;
|
|
28
|
+
disableQueue?: boolean;
|
|
28
29
|
toJSON(): SerializedAccount<SignerData, Metadata>;
|
|
29
30
|
}
|
|
30
31
|
export interface IAccountConstructor<Signer extends Nip07Interface, SignerData, Metadata extends unknown> {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-accounts",
|
|
3
|
-
"version": "0.0.0-next-
|
|
3
|
+
"version": "0.0.0-next-20250125183855",
|
|
4
4
|
"description": "A simple nostr account management system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@noble/hashes": "^1.5.0",
|
|
35
|
-
"applesauce-signer": "0.0.0-next-
|
|
35
|
+
"applesauce-signer": "0.0.0-next-20250125183855",
|
|
36
36
|
"nanoid": "^5.0.9",
|
|
37
37
|
"nostr-tools": "^2.10.3",
|
|
38
38
|
"rxjs": "^7.8.1"
|