applesauce-accounts 0.0.0-next-20250124224944 → 0.0.0-next-20250125174815

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 CHANGED
@@ -1,14 +1,18 @@
1
1
  import { Nip07Interface } from "applesauce-signer";
2
2
  import { EventTemplate, IAccount, SerializedAccount } from "./types.js";
3
+ import { BehaviorSubject } from "rxjs";
4
+ import { NostrEvent } from "nostr-tools";
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
- metadata?: Metadata;
11
+ /** Use a queue for sign and encryption/decryption requests so that there is only one request at a time */
12
+ queueRequests: 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,13 @@ 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<import("nostr-tools").Event>;
29
+ signEvent(template: EventTemplate): Promise<NostrEvent> | NostrEvent;
30
+ /** Resets the request queue */
31
+ resetQueue(): void;
32
+ /** internal queue */
33
+ protected queueLength: number;
34
+ protected lock: Promise<any> | null;
35
+ protected waitForLock<T>(fn: () => Promise<T> | T): Promise<T> | T;
26
36
  }
package/dist/account.js CHANGED
@@ -1,14 +1,20 @@
1
1
  import { nanoid } from "nanoid";
2
- // errors
2
+ import { BehaviorSubject } from "rxjs";
3
3
  export class SignerMismatchError extends Error {
4
4
  }
5
- export class AccountLockedError extends Error {
6
- }
7
5
  export class BaseAccount {
8
6
  pubkey;
9
7
  signer;
10
8
  id = nanoid(8);
11
- metadata;
9
+ /** Use a queue for sign and encryption/decryption requests so that there is only one request at a time */
10
+ queueRequests = true;
11
+ metadata$ = new BehaviorSubject(undefined);
12
+ get metadata() {
13
+ return this.metadata$.value;
14
+ }
15
+ set metadata(metadata) {
16
+ this.metadata$.next(metadata);
17
+ }
12
18
  // encryption interfaces
13
19
  nip04;
14
20
  nip44;
@@ -19,20 +25,20 @@ export class BaseAccount {
19
25
  if (this.signer.nip04) {
20
26
  this.nip04 = {
21
27
  encrypt: (pubkey, plaintext) => {
22
- return this.signer.nip04.encrypt(pubkey, plaintext);
28
+ return this.waitForLock(() => this.signer.nip04.encrypt(pubkey, plaintext));
23
29
  },
24
30
  decrypt: (pubkey, plaintext) => {
25
- return this.signer.nip04.decrypt(pubkey, plaintext);
31
+ return this.waitForLock(() => this.signer.nip04.decrypt(pubkey, plaintext));
26
32
  },
27
33
  };
28
34
  }
29
35
  if (this.signer.nip44) {
30
36
  this.nip44 = {
31
37
  encrypt: (pubkey, plaintext) => {
32
- return this.signer.nip44.encrypt(pubkey, plaintext);
38
+ return this.waitForLock(() => this.signer.nip44.encrypt(pubkey, plaintext));
33
39
  },
34
40
  decrypt: (pubkey, plaintext) => {
35
- return this.signer.nip44.decrypt(pubkey, plaintext);
41
+ return this.waitForLock(() => this.signer.nip44.decrypt(pubkey, plaintext));
36
42
  },
37
43
  };
38
44
  }
@@ -42,21 +48,75 @@ export class BaseAccount {
42
48
  throw new Error("Not implemented");
43
49
  }
44
50
  /** Gets the pubkey from the signer */
45
- async getPublicKey() {
46
- // this.checkLocked();
47
- const signerKey = await this.signer.getPublicKey();
48
- if (this.pubkey !== signerKey)
49
- throw new Error("Account signer mismatch");
50
- return this.pubkey;
51
+ getPublicKey() {
52
+ const result = this.signer.getPublicKey();
53
+ if (result instanceof Promise)
54
+ return result.then((pubkey) => {
55
+ if (this.pubkey !== pubkey)
56
+ throw new SignerMismatchError("Account signer mismatch");
57
+ return pubkey;
58
+ });
59
+ else {
60
+ if (this.pubkey !== result)
61
+ throw new SignerMismatchError("Account signer mismatch");
62
+ return result;
63
+ }
51
64
  }
52
65
  /** sign the event and make sure its signed with the correct pubkey */
53
- async signEvent(template) {
54
- // this.checkLocked();
66
+ signEvent(template) {
55
67
  if (!Reflect.has(template, "pubkey"))
56
68
  Reflect.set(template, "pubkey", this.pubkey);
57
- const signed = await this.signer.signEvent(template);
58
- if (signed.pubkey !== this.pubkey)
59
- throw new SignerMismatchError("Signer signed with wrong pubkey");
60
- return signed;
69
+ return this.waitForLock(() => {
70
+ const result = this.signer.signEvent(template);
71
+ if (result instanceof Promise)
72
+ return result.then((signed) => {
73
+ if (signed.pubkey !== this.pubkey)
74
+ throw new SignerMismatchError("Signer signed with wrong pubkey");
75
+ return signed;
76
+ });
77
+ else {
78
+ if (result.pubkey !== this.pubkey)
79
+ throw new SignerMismatchError("Signer signed with wrong pubkey");
80
+ return result;
81
+ }
82
+ });
83
+ }
84
+ /** Resets the request queue */
85
+ resetQueue() {
86
+ this.lock = null;
87
+ this.queueLength = 0;
88
+ }
89
+ /** internal queue */
90
+ queueLength = 0;
91
+ lock = null;
92
+ waitForLock(fn) {
93
+ if (!this.queueRequests)
94
+ return fn();
95
+ // if there is already a pending request, wait for it
96
+ if (this.lock) {
97
+ // create a new promise that runs after the lock
98
+ const p = this.lock
99
+ .then(() => fn())
100
+ .finally(() => {
101
+ // shorten the queue
102
+ this.queueLength--;
103
+ // if this was the last request, remove the lock
104
+ if (this.queueLength === 0)
105
+ this.lock = null;
106
+ });
107
+ // set the lock the new promise
108
+ this.lock = p;
109
+ this.queueLength++;
110
+ return p;
111
+ }
112
+ else {
113
+ const result = fn();
114
+ // if the result is async, set the new lock
115
+ if (result instanceof Promise) {
116
+ this.lock = result;
117
+ this.queueLength = 1;
118
+ }
119
+ return result;
120
+ }
61
121
  }
62
122
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,56 @@
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
+ account.signEvent({ kind: 1, content: "first", created_at: 0, tags: [] });
21
+ account.signEvent({ kind: 1, content: "second", created_at: 0, tags: [] });
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
+ });
33
+ it("should not use queueing if its disabled", async () => {
34
+ const account = new BaseAccount(await signer.getPublicKey(), signer);
35
+ account.queueRequests = false;
36
+ let resolve = [];
37
+ vi.spyOn(signer, "signEvent").mockImplementation(() => {
38
+ return new Promise((res) => {
39
+ resolve.push(() => res(finalizeEvent({ kind: 1, content: "mock", created_at: 0, tags: [] }, signer.key)));
40
+ });
41
+ });
42
+ // make two signing requests
43
+ account.signEvent({ kind: 1, content: "first", created_at: 0, tags: [] });
44
+ account.signEvent({ kind: 1, content: "second", created_at: 0, tags: [] });
45
+ expect(signer.signEvent).toHaveBeenCalledTimes(2);
46
+ expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "first" }));
47
+ expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "second" }));
48
+ // resolve first
49
+ resolve.shift()?.();
50
+ // wait next tick
51
+ await new Promise((res) => setTimeout(res, 0));
52
+ // resolve second
53
+ resolve.shift()?.();
54
+ });
55
+ });
56
+ });
@@ -1,7 +1,7 @@
1
- import ExtensionAccount from "./extension-account.js";
2
- import PasswordAccount from "./password-account.js";
3
- import ReadonlyAccount from "./readonly-account.js";
4
- import SimpleAccount from "./simple-account.js";
1
+ import { ExtensionAccount } from "./extension-account.js";
2
+ import { PasswordAccount } from "./password-account.js";
3
+ import { ReadonlyAccount } from "./readonly-account.js";
4
+ import { SimpleAccount } from "./simple-account.js";
5
5
  /** Registers the most common account types to a account manager */
6
6
  export function registerCommonAccountTypes(manager) {
7
7
  manager.registerType(ExtensionAccount);
@@ -1,7 +1,7 @@
1
1
  import { ExtensionSigner } from "applesauce-signer/signers/extension-signer";
2
2
  import { BaseAccount } from "../account.js";
3
3
  import { SerializedAccount } from "../types.js";
4
- export default class ExtensionAccount<Metadata extends unknown> extends BaseAccount<ExtensionSigner, void, Metadata> {
4
+ export declare class ExtensionAccount<Metadata extends unknown> extends BaseAccount<ExtensionSigner, void, Metadata> {
5
5
  signer: ExtensionSigner;
6
6
  static type: string;
7
7
  constructor(pubkey: string, signer: ExtensionSigner);
@@ -1,6 +1,6 @@
1
1
  import { ExtensionSigner } from "applesauce-signer/signers/extension-signer";
2
2
  import { BaseAccount } from "../account.js";
3
- export default class ExtensionAccount extends BaseAccount {
3
+ export class ExtensionAccount extends BaseAccount {
4
4
  signer;
5
5
  static type = "extension";
6
6
  constructor(pubkey, signer) {
@@ -4,7 +4,7 @@ import { SerializedAccount } from "../types.js";
4
4
  type SignerData = {
5
5
  ncryptsec: string;
6
6
  };
7
- export default class PasswordAccount<Metadata extends unknown> extends BaseAccount<PasswordSigner, SignerData, Metadata> {
7
+ export declare class PasswordAccount<Metadata extends unknown> extends BaseAccount<PasswordSigner, SignerData, Metadata> {
8
8
  static type: string;
9
9
  get unlocked(): boolean;
10
10
  /** called when PasswordAccount.unlock is called without a password */
@@ -1,6 +1,6 @@
1
1
  import { PasswordSigner } from "applesauce-signer/signers/password-signer";
2
2
  import { BaseAccount } from "../account.js";
3
- export default class PasswordAccount extends BaseAccount {
3
+ export class PasswordAccount extends BaseAccount {
4
4
  static type = "ncryptsec";
5
5
  get unlocked() {
6
6
  return this.signer.unlocked;
@@ -2,7 +2,7 @@ import { ReadonlySigner } from "applesauce-signer/signers/readonly-signer";
2
2
  import { BaseAccount } from "../account.js";
3
3
  import { SerializedAccount } from "../types.js";
4
4
  /** An account that cannot sign or encrypt anything */
5
- export default class ReadonlyAccount<Metadata extends unknown> extends BaseAccount<ReadonlySigner, void, Metadata> {
5
+ export declare class ReadonlyAccount<Metadata extends unknown> extends BaseAccount<ReadonlySigner, void, Metadata> {
6
6
  static type: string;
7
7
  toJSON(): {
8
8
  type: string;
@@ -1,7 +1,7 @@
1
1
  import { ReadonlySigner } from "applesauce-signer/signers/readonly-signer";
2
2
  import { BaseAccount } from "../account.js";
3
3
  /** An account that cannot sign or encrypt anything */
4
- export default class ReadonlyAccount extends BaseAccount {
4
+ export class ReadonlyAccount extends BaseAccount {
5
5
  static type = "readonly";
6
6
  toJSON() {
7
7
  return {
@@ -2,7 +2,7 @@ import { SerialPortSigner } from "applesauce-signer/signers/serial-port-signer";
2
2
  import { BaseAccount } from "../account.js";
3
3
  import { SerializedAccount } from "../types.js";
4
4
  /** An account for SerialPortSigner */
5
- export default class SerialPortAccount<Metadata extends unknown> extends BaseAccount<SerialPortSigner, void, Metadata> {
5
+ export declare class SerialPortAccount<Metadata extends unknown> extends BaseAccount<SerialPortSigner, void, Metadata> {
6
6
  static type: string;
7
7
  unlock(): Promise<boolean>;
8
8
  toJSON(): SerializedAccount<void, Metadata>;
@@ -1,7 +1,7 @@
1
1
  import { SerialPortSigner } from "applesauce-signer/signers/serial-port-signer";
2
2
  import { BaseAccount } from "../account.js";
3
3
  /** An account for SerialPortSigner */
4
- export default class SerialPortAccount extends BaseAccount {
4
+ export class SerialPortAccount extends BaseAccount {
5
5
  static type = "serial-port";
6
6
  async unlock() {
7
7
  try {
@@ -4,7 +4,7 @@ import { SerializedAccount } from "../types.js";
4
4
  type SignerData = {
5
5
  key: string;
6
6
  };
7
- export default class SimpleAccount<Metadata extends unknown> extends BaseAccount<SimpleSigner, SignerData, Metadata> {
7
+ export declare class SimpleAccount<Metadata extends unknown> extends BaseAccount<SimpleSigner, SignerData, Metadata> {
8
8
  static type: string;
9
9
  toJSON(): SerializedAccount<SignerData, Metadata>;
10
10
  static fromJSON<Metadata extends unknown>(json: SerializedAccount<SignerData, Metadata>): SimpleAccount<Metadata>;
@@ -2,7 +2,7 @@ import { getPublicKey } from "nostr-tools";
2
2
  import { SimpleSigner } from "applesauce-signer/signers/simple-signer";
3
3
  import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
4
4
  import { BaseAccount } from "../account.js";
5
- export default class SimpleAccount extends BaseAccount {
5
+ export class SimpleAccount extends BaseAccount {
6
6
  static type = "nsec";
7
7
  toJSON() {
8
8
  return {
package/dist/manager.d.ts CHANGED
@@ -2,9 +2,11 @@ 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>[];
8
10
  /** Add account type class */
9
11
  registerType<S extends Nip07Interface>(accountType: IAccountConstructor<S, any, Metadata>): void;
10
12
  /** Remove account type */
package/dist/manager.js CHANGED
@@ -1,8 +1,14 @@
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
+ }
6
12
  // Account type CRUD
7
13
  /** Add account type class */
8
14
  registerType(accountType) {
@@ -20,62 +26,60 @@ export class AccountManager {
20
26
  /** gets an account in the manager */
21
27
  getAccount(id) {
22
28
  if (typeof id === "string")
23
- return this.accounts.value[id];
24
- else if (this.accounts.value[id.id])
29
+ return this.accounts$.value.find((a) => a.id === id);
30
+ else if (this.accounts$.value.includes(id))
25
31
  return id;
26
32
  else
27
33
  return undefined;
28
34
  }
29
35
  /** Return the first account for a pubkey */
30
36
  getAccountForPubkey(pubkey) {
31
- return Object.values(this.accounts.value).find((account) => account.pubkey === pubkey);
37
+ return Object.values(this.accounts$.value).find((account) => account.pubkey === pubkey);
32
38
  }
33
39
  /** Returns all accounts for a pubkey */
34
40
  getAccountsForPubkey(pubkey) {
35
- return Object.values(this.accounts.value).filter((account) => account.pubkey === pubkey);
41
+ return Object.values(this.accounts$.value).filter((account) => account.pubkey === pubkey);
36
42
  }
37
43
  /** adds an account to the manager */
38
44
  addAccount(account) {
39
45
  if (this.getAccount(account.id))
40
46
  return;
41
- this.accounts.next({
42
- ...this.accounts.value,
47
+ this.accounts$.next({
48
+ ...this.accounts$.value,
43
49
  [account.id]: account,
44
50
  });
45
51
  }
46
52
  /** Removes an account from the manager */
47
53
  removeAccount(account) {
48
54
  const id = typeof account === "string" ? account : account.id;
49
- const next = { ...this.accounts.value };
50
- delete next[id];
51
- this.accounts.next(next);
55
+ this.accounts$.next(this.accounts$.value.filter((a) => a.id !== id));
52
56
  }
53
57
  /** Replaces an account with another */
54
58
  replaceAccount(old, account) {
55
59
  this.addAccount(account);
56
60
  // if the old account was active, switch to the new one
57
61
  const id = typeof account === "string" ? account : account.id;
58
- if (this.active.value?.id === id)
62
+ if (this.active$.value?.id === id)
59
63
  this.setActive(account);
60
64
  this.removeAccount(old);
61
65
  }
62
66
  // Active account methods
63
67
  /** Returns the currently active account */
64
68
  getActive() {
65
- return this.active.value;
69
+ return this.active$.value;
66
70
  }
67
71
  /** Sets the currently active account */
68
72
  setActive(id) {
69
73
  const account = this.getAccount(id);
70
74
  if (!account)
71
75
  throw new Error("Cant find account with that ID");
72
- if (this.active.value?.id !== account.id) {
73
- this.active.next(account);
76
+ if (this.active$.value?.id !== account.id) {
77
+ this.active$.next(account);
74
78
  }
75
79
  }
76
80
  /** Clears the currently active account */
77
81
  clearActive() {
78
- this.active.next(null);
82
+ this.active$.next(null);
79
83
  }
80
84
  // Metadata CRUD
81
85
  /** sets the metadata on an account */
@@ -95,7 +99,7 @@ export class AccountManager {
95
99
  // Serialize / Deserialize
96
100
  /** Returns an array of serialized accounts */
97
101
  toJSON() {
98
- return Array.from(Object.values(this.accounts)).map((account) => account.toJSON());
102
+ return Array.from(Object.values(this.accounts$)).map((account) => account.toJSON());
99
103
  }
100
104
  /**
101
105
  * Restores all accounts from an array of serialized accounts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-accounts",
3
- "version": "0.0.0-next-20250124224944",
3
+ "version": "0.0.0-next-20250125174815",
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-20250124224944",
35
+ "applesauce-signer": "0.0.0-next-20250125174815",
36
36
  "nanoid": "^5.0.9",
37
37
  "nostr-tools": "^2.10.3",
38
38
  "rxjs": "^7.8.1"