applesauce-accounts 0.10.0 → 0.11.0
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/README.md +3 -1
- package/dist/account.d.ts +29 -17
- package/dist/account.js +127 -36
- package/dist/account.test.d.ts +1 -0
- package/dist/account.test.js +66 -0
- package/dist/accounts/amber-clipboard-account.d.ts +6 -7
- package/dist/accounts/amber-clipboard-account.js +7 -8
- package/dist/accounts/common.d.ts +3 -0
- package/dist/accounts/common.js +13 -0
- package/dist/accounts/extension-account.d.ts +7 -4
- package/dist/accounts/extension-account.js +12 -6
- package/dist/accounts/index.d.ts +2 -0
- package/dist/accounts/index.js +2 -0
- package/dist/accounts/nostr-connect-account.d.ts +16 -0
- package/dist/accounts/nostr-connect-account.js +34 -0
- package/dist/accounts/password-account.d.ts +16 -10
- package/dist/accounts/password-account.js +22 -19
- package/dist/accounts/readonly-account.d.ts +6 -5
- package/dist/accounts/readonly-account.js +10 -10
- package/dist/accounts/serial-port-account.d.ts +5 -5
- package/dist/accounts/serial-port-account.js +7 -7
- package/dist/accounts/simple-account.d.ts +8 -9
- package/dist/accounts/simple-account.js +16 -13
- package/dist/manager.d.ts +49 -1
- package/dist/manager.js +163 -0
- package/dist/manager.test.d.ts +1 -0
- package/dist/manager.test.js +58 -0
- package/dist/types.d.ts +28 -13
- package/package.json +11 -7
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
# applesauce-accounts
|
|
2
2
|
|
|
3
|
-
A simple nostr account management system
|
|
3
|
+
A simple nostr account management system built on top of [applesauce-signers](https://hzrd149.github.io/applesauce/signers/installation.html)
|
|
4
|
+
|
|
5
|
+
See [documentation](https://hzrd149.github.io/applesauce/signers/signers.html)
|
package/dist/account.d.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { Nip07Interface } from "applesauce-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Nip07Interface } from "applesauce-signers";
|
|
2
|
+
import { BehaviorSubject } from "rxjs";
|
|
3
|
+
import { NostrEvent } from "nostr-tools";
|
|
4
|
+
import { EventTemplate, IAccount, SerializedAccount } from "./types.js";
|
|
4
5
|
export declare class SignerMismatchError extends Error {
|
|
5
6
|
}
|
|
6
|
-
export declare class
|
|
7
|
-
}
|
|
8
|
-
export declare class BaseAccount<T extends string, S> implements IAccount<T, S> {
|
|
7
|
+
export declare class BaseAccount<Signer extends Nip07Interface, SignerData, Metadata extends unknown> implements IAccount<Signer, SignerData, Metadata> {
|
|
9
8
|
pubkey: string;
|
|
10
|
-
signer:
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
signer: Signer;
|
|
10
|
+
id: string;
|
|
11
|
+
get type(): string;
|
|
12
|
+
/** Disable request queueing */
|
|
13
|
+
disableQueue?: boolean;
|
|
14
|
+
metadata$: BehaviorSubject<Metadata | undefined>;
|
|
15
|
+
get metadata(): Metadata | undefined;
|
|
16
|
+
set metadata(metadata: Metadata);
|
|
13
17
|
nip04?: {
|
|
14
18
|
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
|
|
15
19
|
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
|
@@ -18,14 +22,22 @@ export declare class BaseAccount<T extends string, S> implements IAccount<T, S>
|
|
|
18
22
|
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
|
|
19
23
|
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
|
20
24
|
} | undefined;
|
|
21
|
-
constructor(pubkey: string, signer:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
constructor(pubkey: string, signer: Signer);
|
|
26
|
+
toJSON(): SerializedAccount<SignerData, Metadata>;
|
|
27
|
+
/** Adds the common fields to the serialized output of a toJSON method */
|
|
28
|
+
protected saveCommonFields(json: Omit<SerializedAccount<SignerData, Metadata>, "id" | "type" | "metadata" | "pubkey">): SerializedAccount<SignerData, Metadata>;
|
|
29
|
+
/** Sets an accounts id and metadata. NOTE: This should only be used in fromJSON methods */
|
|
30
|
+
static loadCommonFields<T extends IAccount>(account: T, json: SerializedAccount<any, any>): T;
|
|
27
31
|
/** Gets the pubkey from the signer */
|
|
28
|
-
getPublicKey(): Promise<string>;
|
|
32
|
+
getPublicKey(): string | Promise<string>;
|
|
29
33
|
/** sign the event and make sure its signed with the correct pubkey */
|
|
30
|
-
signEvent(template: EventTemplate): Promise<
|
|
34
|
+
signEvent(template: EventTemplate): Promise<NostrEvent> | NostrEvent;
|
|
35
|
+
/** Aborts all pending requests in the queue */
|
|
36
|
+
abortQueue(reason: Error): void;
|
|
37
|
+
/** internal queue */
|
|
38
|
+
protected queueLength: number;
|
|
39
|
+
protected lock: Promise<any> | null;
|
|
40
|
+
protected abort: AbortController | null;
|
|
41
|
+
protected reduceQueue(): void;
|
|
42
|
+
protected waitForLock<T>(fn: () => Promise<T> | T): Promise<T> | T;
|
|
31
43
|
}
|
package/dist/account.js
CHANGED
|
@@ -1,13 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { nanoid } from "nanoid";
|
|
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
|
+
});
|
|
3
23
|
}
|
|
4
|
-
export class
|
|
24
|
+
export class SignerMismatchError extends Error {
|
|
5
25
|
}
|
|
6
26
|
export class BaseAccount {
|
|
7
27
|
pubkey;
|
|
8
28
|
signer;
|
|
9
|
-
|
|
10
|
-
|
|
29
|
+
id = nanoid(8);
|
|
30
|
+
get type() {
|
|
31
|
+
const cls = Reflect.getPrototypeOf(this).constructor;
|
|
32
|
+
return cls.type;
|
|
33
|
+
}
|
|
34
|
+
/** Disable request queueing */
|
|
35
|
+
disableQueue;
|
|
36
|
+
metadata$ = new BehaviorSubject(undefined);
|
|
37
|
+
get metadata() {
|
|
38
|
+
return this.metadata$.value;
|
|
39
|
+
}
|
|
40
|
+
set metadata(metadata) {
|
|
41
|
+
this.metadata$.next(metadata);
|
|
42
|
+
}
|
|
11
43
|
// encryption interfaces
|
|
12
44
|
nip04;
|
|
13
45
|
nip44;
|
|
@@ -18,60 +50,119 @@ export class BaseAccount {
|
|
|
18
50
|
if (this.signer.nip04) {
|
|
19
51
|
this.nip04 = {
|
|
20
52
|
encrypt: (pubkey, plaintext) => {
|
|
21
|
-
this.
|
|
22
|
-
return this.signer.nip04.encrypt(pubkey, plaintext);
|
|
53
|
+
return this.waitForLock(() => this.signer.nip04.encrypt(pubkey, plaintext));
|
|
23
54
|
},
|
|
24
55
|
decrypt: (pubkey, plaintext) => {
|
|
25
|
-
this.
|
|
26
|
-
return this.signer.nip04.decrypt(pubkey, plaintext);
|
|
56
|
+
return this.waitForLock(() => this.signer.nip04.decrypt(pubkey, plaintext));
|
|
27
57
|
},
|
|
28
58
|
};
|
|
29
59
|
}
|
|
30
60
|
if (this.signer.nip44) {
|
|
31
61
|
this.nip44 = {
|
|
32
62
|
encrypt: (pubkey, plaintext) => {
|
|
33
|
-
this.
|
|
34
|
-
return this.signer.nip44.encrypt(pubkey, plaintext);
|
|
63
|
+
return this.waitForLock(() => this.signer.nip44.encrypt(pubkey, plaintext));
|
|
35
64
|
},
|
|
36
65
|
decrypt: (pubkey, plaintext) => {
|
|
37
|
-
this.
|
|
38
|
-
return this.signer.nip44.decrypt(pubkey, plaintext);
|
|
66
|
+
return this.waitForLock(() => this.signer.nip44.decrypt(pubkey, plaintext));
|
|
39
67
|
},
|
|
40
68
|
};
|
|
41
69
|
}
|
|
42
70
|
}
|
|
43
|
-
async unlock() {
|
|
44
|
-
this.locked = false;
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
lock() {
|
|
48
|
-
this.locked = true;
|
|
49
|
-
}
|
|
50
71
|
// This should be overwritten by a sub class
|
|
51
72
|
toJSON() {
|
|
52
73
|
throw new Error("Not implemented");
|
|
53
74
|
}
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
75
|
+
/** Adds the common fields to the serialized output of a toJSON method */
|
|
76
|
+
saveCommonFields(json) {
|
|
77
|
+
return { ...json, id: this.id, pubkey: this.pubkey, metadata: this.metadata, type: this.type };
|
|
78
|
+
}
|
|
79
|
+
/** Sets an accounts id and metadata. NOTE: This should only be used in fromJSON methods */
|
|
80
|
+
static loadCommonFields(account, json) {
|
|
81
|
+
if (json.id)
|
|
82
|
+
account.id = json.id;
|
|
83
|
+
if (json.metadata)
|
|
84
|
+
account.metadata = json.metadata;
|
|
85
|
+
return account;
|
|
58
86
|
}
|
|
59
87
|
/** Gets the pubkey from the signer */
|
|
60
|
-
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
88
|
+
getPublicKey() {
|
|
89
|
+
const result = this.signer.getPublicKey();
|
|
90
|
+
if (result instanceof Promise)
|
|
91
|
+
return result.then((pubkey) => {
|
|
92
|
+
if (this.pubkey !== pubkey)
|
|
93
|
+
throw new SignerMismatchError("Account signer mismatch");
|
|
94
|
+
return pubkey;
|
|
95
|
+
});
|
|
96
|
+
else {
|
|
97
|
+
if (this.pubkey !== result)
|
|
98
|
+
throw new SignerMismatchError("Account signer mismatch");
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
66
101
|
}
|
|
67
102
|
/** sign the event and make sure its signed with the correct pubkey */
|
|
68
|
-
|
|
69
|
-
this.checkLocked();
|
|
103
|
+
signEvent(template) {
|
|
70
104
|
if (!Reflect.has(template, "pubkey"))
|
|
71
105
|
Reflect.set(template, "pubkey", this.pubkey);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
106
|
+
return this.waitForLock(() => {
|
|
107
|
+
const result = this.signer.signEvent(template);
|
|
108
|
+
if (result instanceof Promise)
|
|
109
|
+
return result.then((signed) => {
|
|
110
|
+
if (signed.pubkey !== this.pubkey)
|
|
111
|
+
throw new SignerMismatchError("Signer signed with wrong pubkey");
|
|
112
|
+
return signed;
|
|
113
|
+
});
|
|
114
|
+
else {
|
|
115
|
+
if (result.pubkey !== this.pubkey)
|
|
116
|
+
throw new SignerMismatchError("Signer signed with wrong pubkey");
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** Aborts all pending requests in the queue */
|
|
122
|
+
abortQueue(reason) {
|
|
123
|
+
if (this.abort)
|
|
124
|
+
this.abort.abort(reason);
|
|
125
|
+
}
|
|
126
|
+
/** internal queue */
|
|
127
|
+
queueLength = 0;
|
|
128
|
+
lock = null;
|
|
129
|
+
abort = null;
|
|
130
|
+
reduceQueue() {
|
|
131
|
+
// shorten the queue
|
|
132
|
+
this.queueLength--;
|
|
133
|
+
// if this was the last request, remove the lock
|
|
134
|
+
if (this.queueLength === 0) {
|
|
135
|
+
this.lock = null;
|
|
136
|
+
this.abort = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
waitForLock(fn) {
|
|
140
|
+
if (this.disableQueue)
|
|
141
|
+
return fn();
|
|
142
|
+
// if there is already a pending request, wait for it
|
|
143
|
+
if (this.lock && this.abort) {
|
|
144
|
+
// create a new promise that runs after the lock
|
|
145
|
+
const p = wrapInSignal(this.lock.then(() => {
|
|
146
|
+
// if the abort signal is triggered, don't call the signer
|
|
147
|
+
this.abort?.signal.throwIfAborted();
|
|
148
|
+
return fn();
|
|
149
|
+
}), this.abort.signal);
|
|
150
|
+
// set the lock the new promise that ignores errors
|
|
151
|
+
this.lock = p.catch(() => { }).finally(this.reduceQueue.bind(this));
|
|
152
|
+
this.queueLength++;
|
|
153
|
+
return p;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const result = fn();
|
|
157
|
+
// if the result is async, set the new lock
|
|
158
|
+
if (result instanceof Promise) {
|
|
159
|
+
this.abort = new AbortController();
|
|
160
|
+
const p = wrapInSignal(result, this.abort.signal);
|
|
161
|
+
// set the lock the new promise that ignores errors
|
|
162
|
+
this.lock = p.catch(() => { }).finally(this.reduceQueue.bind(this));
|
|
163
|
+
this.queueLength = 1;
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
76
167
|
}
|
|
77
168
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { BaseAccount } from "./account.js";
|
|
3
|
+
import { SimpleSigner } from "applesauce-signers";
|
|
4
|
+
import { finalizeEvent, generateSecretKey } from "nostr-tools";
|
|
5
|
+
import { SimpleAccount } from "./accounts/simple-account.js";
|
|
6
|
+
describe("BaseAccount", () => {
|
|
7
|
+
let signer;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
signer = new SimpleSigner();
|
|
10
|
+
});
|
|
11
|
+
describe("request queue", () => {
|
|
12
|
+
it("should queue signing requests by default", async () => {
|
|
13
|
+
const account = new BaseAccount(await signer.getPublicKey(), signer);
|
|
14
|
+
let resolve = [];
|
|
15
|
+
vi.spyOn(signer, "signEvent").mockImplementation(() => {
|
|
16
|
+
return new Promise((res) => {
|
|
17
|
+
resolve.push(() => res(finalizeEvent({ kind: 1, content: "mock", created_at: 0, tags: [] }, signer.key)));
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
// make two signing requests
|
|
21
|
+
expect(account.signEvent({ kind: 1, content: "first", created_at: 0, tags: [] })).toEqual(expect.any(Promise));
|
|
22
|
+
expect(account.signEvent({ kind: 1, content: "second", created_at: 0, tags: [] })).toEqual(expect.any(Promise));
|
|
23
|
+
expect(signer.signEvent).toHaveBeenCalledOnce();
|
|
24
|
+
expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "first" }));
|
|
25
|
+
// resolve first
|
|
26
|
+
resolve.shift()?.();
|
|
27
|
+
// wait next tick
|
|
28
|
+
await new Promise((res) => setTimeout(res, 0));
|
|
29
|
+
expect(signer.signEvent).toHaveBeenCalledTimes(2);
|
|
30
|
+
expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "second" }));
|
|
31
|
+
// resolve second
|
|
32
|
+
resolve.shift()?.();
|
|
33
|
+
// wait next tick
|
|
34
|
+
await new Promise((res) => setTimeout(res, 0));
|
|
35
|
+
expect(Reflect.get(account, "queueLength")).toBe(0);
|
|
36
|
+
expect(Reflect.get(account, "lock")).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
it("should cancel queue if request throws", () => { });
|
|
39
|
+
it("should not use queueing if its disabled", async () => {
|
|
40
|
+
const account = new BaseAccount(await signer.getPublicKey(), signer);
|
|
41
|
+
account.disableQueue = true;
|
|
42
|
+
let resolve = [];
|
|
43
|
+
vi.spyOn(signer, "signEvent").mockImplementation(() => {
|
|
44
|
+
return new Promise((res) => {
|
|
45
|
+
resolve.push(() => res(finalizeEvent({ kind: 1, content: "mock", created_at: 0, tags: [] }, signer.key)));
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
// make two signing requests
|
|
49
|
+
account.signEvent({ kind: 1, content: "first", created_at: 0, tags: [] });
|
|
50
|
+
account.signEvent({ kind: 1, content: "second", created_at: 0, tags: [] });
|
|
51
|
+
expect(Reflect.get(account, "lock")).toBeNull();
|
|
52
|
+
expect(signer.signEvent).toHaveBeenCalledTimes(2);
|
|
53
|
+
expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "first" }));
|
|
54
|
+
expect(signer.signEvent).toHaveBeenCalledWith(expect.objectContaining({ content: "second" }));
|
|
55
|
+
// resolve both
|
|
56
|
+
resolve.shift()?.();
|
|
57
|
+
resolve.shift()?.();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe("type", () => {
|
|
61
|
+
it("should return static account type", () => {
|
|
62
|
+
const account = SimpleAccount.fromKey(generateSecretKey());
|
|
63
|
+
expect(account.type).toBe("nsec");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { AmberClipboardSigner } from "applesauce-
|
|
1
|
+
import { AmberClipboardSigner } from "applesauce-signers/signers/amber-clipboard-signer";
|
|
2
2
|
import { BaseAccount } from "../account.js";
|
|
3
|
-
import {
|
|
3
|
+
import { SerializedAccount } from "../types.js";
|
|
4
4
|
/** An account for the amber clipboard api */
|
|
5
|
-
export declare class AmberClipboardAccount extends BaseAccount<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
static fromJSON(json: SerializedAccount<"amber-clipboard", void>): IAccount<"amber-clipboard", void>;
|
|
5
|
+
export declare class AmberClipboardAccount<Metadata extends unknown> extends BaseAccount<AmberClipboardSigner, void, Metadata> {
|
|
6
|
+
static readonly type = "amber-clipboard";
|
|
7
|
+
toJSON(): SerializedAccount<void, Metadata>;
|
|
8
|
+
static fromJSON<Metadata extends unknown>(json: SerializedAccount<void, Metadata>): AmberClipboardAccount<Metadata>;
|
|
10
9
|
}
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { AmberClipboardSigner } from "applesauce-
|
|
1
|
+
import { AmberClipboardSigner } from "applesauce-signers/signers/amber-clipboard-signer";
|
|
2
2
|
import { BaseAccount } from "../account.js";
|
|
3
3
|
/** An account for the amber clipboard api */
|
|
4
4
|
export class AmberClipboardAccount extends BaseAccount {
|
|
5
|
-
|
|
6
|
-
constructor(pubkey, signer) {
|
|
7
|
-
super(pubkey, signer);
|
|
8
|
-
this.signer = signer;
|
|
9
|
-
}
|
|
5
|
+
static type = "amber-clipboard";
|
|
10
6
|
toJSON() {
|
|
11
|
-
return {
|
|
7
|
+
return super.saveCommonFields({
|
|
8
|
+
signer: undefined,
|
|
9
|
+
});
|
|
12
10
|
}
|
|
13
11
|
static fromJSON(json) {
|
|
14
|
-
|
|
12
|
+
const account = new AmberClipboardAccount(json.pubkey, new AmberClipboardSigner());
|
|
13
|
+
return super.loadCommonFields(account, json);
|
|
15
14
|
}
|
|
16
15
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ExtensionAccount } from "./extension-account.js";
|
|
2
|
+
import { NostrConnectAccount } from "./nostr-connect-account.js";
|
|
3
|
+
import { PasswordAccount } from "./password-account.js";
|
|
4
|
+
import { ReadonlyAccount } from "./readonly-account.js";
|
|
5
|
+
import { SimpleAccount } from "./simple-account.js";
|
|
6
|
+
/** Registers the most common account types to a account manager */
|
|
7
|
+
export function registerCommonAccountTypes(manager) {
|
|
8
|
+
manager.registerType(ExtensionAccount);
|
|
9
|
+
manager.registerType(PasswordAccount);
|
|
10
|
+
manager.registerType(ReadonlyAccount);
|
|
11
|
+
manager.registerType(SimpleAccount);
|
|
12
|
+
manager.registerType(NostrConnectAccount);
|
|
13
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { ExtensionSigner } from "applesauce-signers/signers/extension-signer";
|
|
1
2
|
import { BaseAccount } from "../account.js";
|
|
2
3
|
import { SerializedAccount } from "../types.js";
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
export declare class ExtensionAccount<Metadata extends unknown> extends BaseAccount<ExtensionSigner, void, Metadata> {
|
|
5
|
+
signer: ExtensionSigner;
|
|
6
|
+
static readonly type = "extension";
|
|
7
|
+
constructor(pubkey: string, signer: ExtensionSigner);
|
|
8
|
+
toJSON(): SerializedAccount<void, Metadata>;
|
|
9
|
+
static fromJSON<Metadata extends unknown>(json: SerializedAccount<void, Metadata>): ExtensionAccount<Metadata>;
|
|
7
10
|
}
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { ExtensionSigner } from "applesauce-
|
|
1
|
+
import { ExtensionSigner } from "applesauce-signers/signers/extension-signer";
|
|
2
2
|
import { BaseAccount } from "../account.js";
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
export class ExtensionAccount extends BaseAccount {
|
|
4
|
+
signer;
|
|
5
|
+
static type = "extension";
|
|
6
|
+
constructor(pubkey, signer) {
|
|
7
|
+
super(pubkey, signer || new ExtensionSigner());
|
|
8
|
+
this.signer = signer;
|
|
6
9
|
}
|
|
7
10
|
toJSON() {
|
|
8
|
-
return {
|
|
11
|
+
return super.saveCommonFields({
|
|
12
|
+
signer: undefined,
|
|
13
|
+
});
|
|
9
14
|
}
|
|
10
15
|
static fromJSON(json) {
|
|
11
|
-
|
|
16
|
+
const account = new ExtensionAccount(json.pubkey, new ExtensionSigner());
|
|
17
|
+
return super.loadCommonFields(account, json);
|
|
12
18
|
}
|
|
13
19
|
}
|
package/dist/accounts/index.d.ts
CHANGED
package/dist/accounts/index.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NostrConnectConnectionMethods, NostrConnectSigner } from "applesauce-signers";
|
|
2
|
+
import { BaseAccount } from "../account.js";
|
|
3
|
+
import { SerializedAccount } from "../types.js";
|
|
4
|
+
export type NostrConnectAccountSignerData = {
|
|
5
|
+
clientKey: string;
|
|
6
|
+
remote: string;
|
|
7
|
+
relays: string[];
|
|
8
|
+
};
|
|
9
|
+
/** An account type for NIP-46 signers */
|
|
10
|
+
export declare class NostrConnectAccount<Metadata extends unknown> extends BaseAccount<NostrConnectSigner, NostrConnectAccountSignerData, Metadata> {
|
|
11
|
+
static readonly type = "nostr-connect";
|
|
12
|
+
toJSON(): SerializedAccount<NostrConnectAccountSignerData, Metadata>;
|
|
13
|
+
/** This is called when NostrConnectAccount.fromJSON needs new connection methods for NostrConnectSigner */
|
|
14
|
+
static createConnectionMethods(): NostrConnectConnectionMethods;
|
|
15
|
+
static fromJSON<Metadata extends unknown>(json: SerializedAccount<NostrConnectAccountSignerData, Metadata>, connection?: NostrConnectConnectionMethods): NostrConnectAccount<Metadata>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NostrConnectSigner, SimpleSigner } from "applesauce-signers";
|
|
2
|
+
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
|
3
|
+
import { BaseAccount } from "../account.js";
|
|
4
|
+
/** An account type for NIP-46 signers */
|
|
5
|
+
export class NostrConnectAccount extends BaseAccount {
|
|
6
|
+
static type = "nostr-connect";
|
|
7
|
+
toJSON() {
|
|
8
|
+
if (!this.signer.remote)
|
|
9
|
+
throw new Error("Cant save NostrConnectAccount when not initialized");
|
|
10
|
+
return super.saveCommonFields({
|
|
11
|
+
signer: {
|
|
12
|
+
clientKey: bytesToHex(this.signer.signer.key),
|
|
13
|
+
remote: this.signer.remote,
|
|
14
|
+
relays: this.signer.relays,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** This is called when NostrConnectAccount.fromJSON needs new connection methods for NostrConnectSigner */
|
|
19
|
+
static createConnectionMethods() {
|
|
20
|
+
throw new Error("Cant create NostrConnectAccount without either passing in connection methods or setting NostrConnectAccount.createConnectionMethods");
|
|
21
|
+
}
|
|
22
|
+
static fromJSON(json, connection) {
|
|
23
|
+
connection = connection || NostrConnectAccount.createConnectionMethods();
|
|
24
|
+
const signer = new NostrConnectSigner({
|
|
25
|
+
...connection,
|
|
26
|
+
relays: json.signer.relays,
|
|
27
|
+
pubkey: json.pubkey,
|
|
28
|
+
remote: json.signer.remote,
|
|
29
|
+
signer: new SimpleSigner(hexToBytes(json.signer.clientKey)),
|
|
30
|
+
});
|
|
31
|
+
const account = new NostrConnectAccount(json.pubkey, signer);
|
|
32
|
+
return super.loadCommonFields(account, json);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { PasswordSigner } from "applesauce-
|
|
1
|
+
import { PasswordSigner } from "applesauce-signers/signers/password-signer";
|
|
2
2
|
import { BaseAccount } from "../account.js";
|
|
3
3
|
import { SerializedAccount } from "../types.js";
|
|
4
|
-
type
|
|
4
|
+
export type PasswordAccountSignerData = {
|
|
5
5
|
ncryptsec: string;
|
|
6
6
|
};
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
unlock
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
export declare class PasswordAccount<Metadata extends unknown> extends BaseAccount<PasswordSigner, PasswordAccountSignerData, Metadata> {
|
|
8
|
+
static readonly type = "ncryptsec";
|
|
9
|
+
get unlocked(): boolean;
|
|
10
|
+
/** called when PasswordAccount.unlock is called without a password */
|
|
11
|
+
static requestUnlockPassword(_account: PasswordAccount<any>): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* Attempt to unlock the signer with a password
|
|
14
|
+
* @throws
|
|
15
|
+
*/
|
|
16
|
+
unlock(password?: string): Promise<void>;
|
|
17
|
+
toJSON(): SerializedAccount<PasswordAccountSignerData, Metadata>;
|
|
18
|
+
static fromJSON<Metadata extends unknown>(json: SerializedAccount<PasswordAccountSignerData, Metadata>): PasswordAccount<Metadata>;
|
|
19
|
+
/** Creates a new PasswordAccount from a ncryptsec string */
|
|
20
|
+
static fromNcryptsec<Metadata extends unknown>(pubkey: string, ncryptsec: string): PasswordAccount<Metadata>;
|
|
14
21
|
}
|
|
15
|
-
export {};
|
|
@@ -1,33 +1,36 @@
|
|
|
1
|
-
import { PasswordSigner } from "applesauce-
|
|
1
|
+
import { PasswordSigner } from "applesauce-signers/signers/password-signer";
|
|
2
2
|
import { BaseAccount } from "../account.js";
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
this.signer = signer;
|
|
3
|
+
export class PasswordAccount extends BaseAccount {
|
|
4
|
+
static type = "ncryptsec";
|
|
5
|
+
get unlocked() {
|
|
6
|
+
return this.signer.unlocked;
|
|
8
7
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
8
|
+
/** called when PasswordAccount.unlock is called without a password */
|
|
9
|
+
static async requestUnlockPassword(_account) {
|
|
10
|
+
throw new Error("Cant unlock PasswordAccount without a password. either pass one in or set PasswordAccount.requestUnlockPassword");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Attempt to unlock the signer with a password
|
|
14
|
+
* @throws
|
|
15
|
+
*/
|
|
16
|
+
async unlock(password) {
|
|
17
|
+
password = password || (await PasswordAccount.requestUnlockPassword(this));
|
|
18
|
+
await this.signer.unlock(password);
|
|
20
19
|
}
|
|
21
20
|
toJSON() {
|
|
22
21
|
if (!this.signer.ncryptsec)
|
|
23
22
|
throw new Error("Cant save account without ncryptsec");
|
|
24
|
-
return
|
|
23
|
+
return super.saveCommonFields({
|
|
24
|
+
signer: { ncryptsec: this.signer.ncryptsec },
|
|
25
|
+
});
|
|
25
26
|
}
|
|
26
27
|
static fromJSON(json) {
|
|
27
28
|
const signer = new PasswordSigner();
|
|
28
29
|
signer.ncryptsec = json.signer.ncryptsec;
|
|
29
|
-
|
|
30
|
+
const account = new PasswordAccount(json.pubkey, signer);
|
|
31
|
+
return super.loadCommonFields(account, json);
|
|
30
32
|
}
|
|
33
|
+
/** Creates a new PasswordAccount from a ncryptsec string */
|
|
31
34
|
static fromNcryptsec(pubkey, ncryptsec) {
|
|
32
35
|
const signer = new PasswordSigner();
|
|
33
36
|
signer.ncryptsec = ncryptsec;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { ReadonlySigner } from "applesauce-
|
|
1
|
+
import { ReadonlySigner } from "applesauce-signers/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
|
|
6
|
-
|
|
7
|
-
toJSON(): SerializedAccount<
|
|
8
|
-
static fromJSON(json: SerializedAccount<
|
|
5
|
+
export declare class ReadonlyAccount<Metadata extends unknown> extends BaseAccount<ReadonlySigner, void, Metadata> {
|
|
6
|
+
static readonly type = "readonly";
|
|
7
|
+
toJSON(): SerializedAccount<void, Metadata>;
|
|
8
|
+
static fromJSON<Metadata extends unknown>(json: SerializedAccount<void, Metadata>): ReadonlyAccount<Metadata>;
|
|
9
|
+
static fromPubkey(pubkey: string): ReadonlyAccount<unknown>;
|
|
9
10
|
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { ReadonlySigner } from "applesauce-
|
|
1
|
+
import { ReadonlySigner } from "applesauce-signers/signers/readonly-signer";
|
|
2
2
|
import { BaseAccount } from "../account.js";
|
|
3
3
|
/** An account that cannot sign or encrypt anything */
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
super(pubkey, signer || new ReadonlySigner(pubkey));
|
|
7
|
-
}
|
|
4
|
+
export class ReadonlyAccount extends BaseAccount {
|
|
5
|
+
static type = "readonly";
|
|
8
6
|
toJSON() {
|
|
9
|
-
return {
|
|
10
|
-
type: "readonly",
|
|
11
|
-
pubkey: this.pubkey,
|
|
7
|
+
return super.saveCommonFields({
|
|
12
8
|
signer: undefined,
|
|
13
|
-
};
|
|
9
|
+
});
|
|
14
10
|
}
|
|
15
11
|
static fromJSON(json) {
|
|
16
|
-
|
|
12
|
+
const account = new ReadonlyAccount(json.pubkey, new ReadonlySigner(json.pubkey));
|
|
13
|
+
return super.loadCommonFields(account, json);
|
|
14
|
+
}
|
|
15
|
+
static fromPubkey(pubkey) {
|
|
16
|
+
return new ReadonlyAccount(pubkey, new ReadonlySigner(pubkey));
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { SerialPortSigner } from "applesauce-
|
|
1
|
+
import { SerialPortSigner } from "applesauce-signers/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
|
|
6
|
-
|
|
5
|
+
export declare class SerialPortAccount<Metadata extends unknown> extends BaseAccount<SerialPortSigner, void, Metadata> {
|
|
6
|
+
static readonly type = "serial-port";
|
|
7
7
|
unlock(): Promise<boolean>;
|
|
8
|
-
toJSON(): SerializedAccount<
|
|
9
|
-
static fromJSON(json: SerializedAccount<
|
|
8
|
+
toJSON(): SerializedAccount<void, Metadata>;
|
|
9
|
+
static fromJSON<Metadata extends unknown>(json: SerializedAccount<void, Metadata>): SerialPortAccount<Metadata>;
|
|
10
10
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { SerialPortSigner } from "applesauce-
|
|
1
|
+
import { SerialPortSigner } from "applesauce-signers/signers/serial-port-signer";
|
|
2
2
|
import { BaseAccount } from "../account.js";
|
|
3
3
|
/** An account for SerialPortSigner */
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
super(pubkey, signer || new SerialPortSigner());
|
|
7
|
-
}
|
|
4
|
+
export class SerialPortAccount extends BaseAccount {
|
|
5
|
+
static type = "serial-port";
|
|
8
6
|
async unlock() {
|
|
9
7
|
try {
|
|
10
8
|
const pubkey = await this.signer.getPublicKey();
|
|
@@ -17,9 +15,11 @@ export default class SerialPortAccount extends BaseAccount {
|
|
|
17
15
|
}
|
|
18
16
|
}
|
|
19
17
|
toJSON() {
|
|
20
|
-
return {
|
|
18
|
+
return super.saveCommonFields({ signer: undefined });
|
|
21
19
|
}
|
|
22
20
|
static fromJSON(json) {
|
|
23
|
-
|
|
21
|
+
const signer = new SerialPortSigner();
|
|
22
|
+
const account = new SerialPortAccount(json.pubkey, signer);
|
|
23
|
+
return super.loadCommonFields(account, json);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { SimpleSigner } from "applesauce-
|
|
1
|
+
import { SimpleSigner } from "applesauce-signers/signers/simple-signer";
|
|
2
2
|
import { BaseAccount } from "../account.js";
|
|
3
3
|
import { SerializedAccount } from "../types.js";
|
|
4
|
-
type
|
|
4
|
+
export type SimpleAccountSignerData = {
|
|
5
5
|
key: string;
|
|
6
6
|
};
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
static
|
|
11
|
-
|
|
12
|
-
static
|
|
7
|
+
export declare class SimpleAccount<Metadata extends unknown> extends BaseAccount<SimpleSigner, SimpleAccountSignerData, Metadata> {
|
|
8
|
+
static readonly type = "nsec";
|
|
9
|
+
toJSON(): SerializedAccount<SimpleAccountSignerData, Metadata>;
|
|
10
|
+
static fromJSON<Metadata extends unknown>(json: SerializedAccount<SimpleAccountSignerData, Metadata>): SimpleAccount<Metadata>;
|
|
11
|
+
static fromKey<Metadata extends unknown>(key: Uint8Array | string): SimpleAccount<Metadata>;
|
|
12
|
+
static generateNew<Metadata extends unknown>(): SimpleAccount<Metadata>;
|
|
13
13
|
}
|
|
14
|
-
export {};
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { getPublicKey } from "nostr-tools";
|
|
2
|
-
import { SimpleSigner } from "applesauce-
|
|
1
|
+
import { generateSecretKey, getPublicKey } from "nostr-tools";
|
|
2
|
+
import { SimpleSigner } from "applesauce-signers/signers/simple-signer";
|
|
3
3
|
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
|
4
4
|
import { BaseAccount } from "../account.js";
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
super(
|
|
9
|
-
|
|
5
|
+
export class SimpleAccount extends BaseAccount {
|
|
6
|
+
static type = "nsec";
|
|
7
|
+
toJSON() {
|
|
8
|
+
return super.saveCommonFields({
|
|
9
|
+
signer: { key: bytesToHex(this.signer.key) },
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
static fromJSON(json) {
|
|
13
|
+
const key = hexToBytes(json.signer.key);
|
|
14
|
+
const account = new SimpleAccount(json.pubkey, new SimpleSigner(key));
|
|
15
|
+
return super.loadCommonFields(account, json);
|
|
10
16
|
}
|
|
11
17
|
static fromKey(key) {
|
|
12
18
|
if (typeof key === "string")
|
|
@@ -14,11 +20,8 @@ export default class SimpleAccount extends BaseAccount {
|
|
|
14
20
|
const pubkey = getPublicKey(key);
|
|
15
21
|
return new SimpleAccount(pubkey, new SimpleSigner(key));
|
|
16
22
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
static fromJSON(json) {
|
|
21
|
-
const key = hexToBytes(json.signer.key);
|
|
22
|
-
return new SimpleAccount(json.pubkey, new SimpleSigner(key));
|
|
23
|
+
static generateNew() {
|
|
24
|
+
const key = generateSecretKey();
|
|
25
|
+
return SimpleAccount.fromKey(key);
|
|
23
26
|
}
|
|
24
27
|
}
|
package/dist/manager.d.ts
CHANGED
|
@@ -1,2 +1,50 @@
|
|
|
1
|
-
|
|
1
|
+
import { Nip07Interface } from "applesauce-signers";
|
|
2
|
+
import { BehaviorSubject } from "rxjs";
|
|
3
|
+
import { IAccount, IAccountConstructor, SerializedAccount } from "./types.js";
|
|
4
|
+
export declare class AccountManager<Metadata extends unknown = any> {
|
|
5
|
+
types: Map<string, IAccountConstructor<any, any, Metadata>>;
|
|
6
|
+
active$: BehaviorSubject<IAccount<any, any, Metadata> | undefined>;
|
|
7
|
+
get active(): IAccount<any, any, Metadata> | undefined;
|
|
8
|
+
accounts$: BehaviorSubject<IAccount<any, any, Metadata>[]>;
|
|
9
|
+
get accounts(): IAccount<any, any, Metadata>[];
|
|
10
|
+
/** Proxy signer for currently active account */
|
|
11
|
+
signer: IAccount<any, any, Metadata>;
|
|
12
|
+
/** Disable request queueing for any accounts added to this manager */
|
|
13
|
+
disableQueue?: boolean;
|
|
14
|
+
constructor();
|
|
15
|
+
/** Add account type class */
|
|
16
|
+
registerType<S extends Nip07Interface>(accountType: IAccountConstructor<S, any, Metadata>): void;
|
|
17
|
+
/** Remove account type */
|
|
18
|
+
unregisterType(type: string): void;
|
|
19
|
+
/** gets an account in the manager */
|
|
20
|
+
getAccount<Signer extends Nip07Interface>(id: string | IAccount<Signer, any, Metadata>): IAccount<Signer, any, Metadata> | undefined;
|
|
21
|
+
/** Return the first account for a pubkey */
|
|
22
|
+
getAccountForPubkey(pubkey: string): IAccount<any, any, Metadata> | undefined;
|
|
23
|
+
/** Returns all accounts for a pubkey */
|
|
24
|
+
getAccountsForPubkey(pubkey: string): IAccount<any, any, Metadata>[];
|
|
25
|
+
/** adds an account to the manager */
|
|
26
|
+
addAccount(account: IAccount<any, any, Metadata>): void;
|
|
27
|
+
/** Removes an account from the manager */
|
|
28
|
+
removeAccount(account: string | IAccount<any, any, Metadata>): void;
|
|
29
|
+
/** Replaces an account with another */
|
|
30
|
+
replaceAccount(old: string | IAccount<any, any, Metadata>, account: IAccount<any, any, Metadata>): void;
|
|
31
|
+
/** Returns the currently active account */
|
|
32
|
+
getActive(): IAccount<any, any, Metadata> | undefined;
|
|
33
|
+
/** Sets the currently active account */
|
|
34
|
+
setActive(id: string | IAccount<any, any, Metadata>): void;
|
|
35
|
+
/** Clears the currently active account */
|
|
36
|
+
clearActive(): void;
|
|
37
|
+
/** sets the metadata on an account */
|
|
38
|
+
setAccountMetadata(id: string | IAccount<any, any, Metadata>, metadata: Metadata): void;
|
|
39
|
+
/** sets the metadata on an account */
|
|
40
|
+
getAccountMetadata(id: string | IAccount<any, any, Metadata>): Metadata | undefined;
|
|
41
|
+
/** Removes all metadata on the account */
|
|
42
|
+
clearAccountMetadata(id: string | IAccount<any, any, Metadata>): void;
|
|
43
|
+
/** Returns an array of serialized accounts */
|
|
44
|
+
toJSON(quite?: boolean): SerializedAccount<any, Metadata>[];
|
|
45
|
+
/**
|
|
46
|
+
* Restores all accounts from an array of serialized accounts
|
|
47
|
+
* NOTE: this will clear all existing accounts
|
|
48
|
+
*/
|
|
49
|
+
fromJSON(accounts: SerializedAccount<any, Metadata>[], quite?: boolean): void;
|
|
2
50
|
}
|
package/dist/manager.js
CHANGED
|
@@ -1,2 +1,165 @@
|
|
|
1
|
+
import { BehaviorSubject } from "rxjs";
|
|
1
2
|
export class AccountManager {
|
|
3
|
+
types = new Map();
|
|
4
|
+
active$ = new BehaviorSubject(undefined);
|
|
5
|
+
get active() {
|
|
6
|
+
return this.active$.value;
|
|
7
|
+
}
|
|
8
|
+
accounts$ = new BehaviorSubject([]);
|
|
9
|
+
get accounts() {
|
|
10
|
+
return this.accounts$.value;
|
|
11
|
+
}
|
|
12
|
+
/** Proxy signer for currently active account */
|
|
13
|
+
signer;
|
|
14
|
+
/** Disable request queueing for any accounts added to this manager */
|
|
15
|
+
disableQueue;
|
|
16
|
+
constructor() {
|
|
17
|
+
this.signer = new Proxy({}, {
|
|
18
|
+
get: (_, p) => {
|
|
19
|
+
if (!this.active)
|
|
20
|
+
throw new Error("No active account");
|
|
21
|
+
return Reflect.get(this.active, p);
|
|
22
|
+
},
|
|
23
|
+
has: (_, p) => {
|
|
24
|
+
if (!this.active)
|
|
25
|
+
throw new Error("No active account");
|
|
26
|
+
return Reflect.has(this.active, p);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// Account type CRUD
|
|
31
|
+
/** Add account type class */
|
|
32
|
+
registerType(accountType) {
|
|
33
|
+
if (!accountType.type)
|
|
34
|
+
throw new Error(`Account class missing static "type" field`);
|
|
35
|
+
if (this.types.has(accountType.type))
|
|
36
|
+
throw new Error(`An account type of ${accountType.type} already exists`);
|
|
37
|
+
this.types.set(accountType.type, accountType);
|
|
38
|
+
}
|
|
39
|
+
/** Remove account type */
|
|
40
|
+
unregisterType(type) {
|
|
41
|
+
this.types.delete(type);
|
|
42
|
+
}
|
|
43
|
+
// Accounts CRUD
|
|
44
|
+
/** gets an account in the manager */
|
|
45
|
+
getAccount(id) {
|
|
46
|
+
if (typeof id === "string")
|
|
47
|
+
return this.accounts$.value.find((a) => a.id === id);
|
|
48
|
+
else if (this.accounts$.value.includes(id))
|
|
49
|
+
return id;
|
|
50
|
+
else
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
/** Return the first account for a pubkey */
|
|
54
|
+
getAccountForPubkey(pubkey) {
|
|
55
|
+
return Object.values(this.accounts$.value).find((account) => account.pubkey === pubkey);
|
|
56
|
+
}
|
|
57
|
+
/** Returns all accounts for a pubkey */
|
|
58
|
+
getAccountsForPubkey(pubkey) {
|
|
59
|
+
return Object.values(this.accounts$.value).filter((account) => account.pubkey === pubkey);
|
|
60
|
+
}
|
|
61
|
+
/** adds an account to the manager */
|
|
62
|
+
addAccount(account) {
|
|
63
|
+
if (this.getAccount(account.id))
|
|
64
|
+
return;
|
|
65
|
+
// copy the disableQueue flag only if its set
|
|
66
|
+
if (this.disableQueue !== undefined && account.disableQueue !== undefined) {
|
|
67
|
+
account.disableQueue = this.disableQueue;
|
|
68
|
+
}
|
|
69
|
+
this.accounts$.next([...this.accounts$.value, account]);
|
|
70
|
+
}
|
|
71
|
+
/** Removes an account from the manager */
|
|
72
|
+
removeAccount(account) {
|
|
73
|
+
const id = typeof account === "string" ? account : account.id;
|
|
74
|
+
this.accounts$.next(this.accounts$.value.filter((a) => a.id !== id));
|
|
75
|
+
}
|
|
76
|
+
/** Replaces an account with another */
|
|
77
|
+
replaceAccount(old, account) {
|
|
78
|
+
this.addAccount(account);
|
|
79
|
+
// if the old account was active, switch to the new one
|
|
80
|
+
const id = typeof account === "string" ? account : account.id;
|
|
81
|
+
if (this.active$.value?.id === id)
|
|
82
|
+
this.setActive(account);
|
|
83
|
+
this.removeAccount(old);
|
|
84
|
+
}
|
|
85
|
+
// Active account methods
|
|
86
|
+
/** Returns the currently active account */
|
|
87
|
+
getActive() {
|
|
88
|
+
return this.active$.value;
|
|
89
|
+
}
|
|
90
|
+
/** Sets the currently active account */
|
|
91
|
+
setActive(id) {
|
|
92
|
+
const account = this.getAccount(id);
|
|
93
|
+
if (!account)
|
|
94
|
+
throw new Error("Cant find account with that ID");
|
|
95
|
+
if (this.active$.value?.id !== account.id) {
|
|
96
|
+
this.active$.next(account);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Clears the currently active account */
|
|
100
|
+
clearActive() {
|
|
101
|
+
this.active$.next(undefined);
|
|
102
|
+
}
|
|
103
|
+
// Metadata CRUD
|
|
104
|
+
/** sets the metadata on an account */
|
|
105
|
+
setAccountMetadata(id, metadata) {
|
|
106
|
+
const account = this.getAccount(id);
|
|
107
|
+
if (!account)
|
|
108
|
+
throw new Error("Cant find account with that ID");
|
|
109
|
+
account.metadata = metadata;
|
|
110
|
+
}
|
|
111
|
+
/** sets the metadata on an account */
|
|
112
|
+
getAccountMetadata(id) {
|
|
113
|
+
const account = this.getAccount(id);
|
|
114
|
+
if (!account)
|
|
115
|
+
throw new Error("Cant find account with that ID");
|
|
116
|
+
return account.metadata;
|
|
117
|
+
}
|
|
118
|
+
/** Removes all metadata on the account */
|
|
119
|
+
clearAccountMetadata(id) {
|
|
120
|
+
const account = this.getAccount(id);
|
|
121
|
+
if (!account)
|
|
122
|
+
throw new Error("Cant find account with that ID");
|
|
123
|
+
account.metadata = undefined;
|
|
124
|
+
}
|
|
125
|
+
// Serialize / Deserialize
|
|
126
|
+
/** Returns an array of serialized accounts */
|
|
127
|
+
toJSON(quite = false) {
|
|
128
|
+
const accounts = [];
|
|
129
|
+
for (const account of this.accounts) {
|
|
130
|
+
try {
|
|
131
|
+
accounts.push(account.toJSON());
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (!quite)
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return accounts;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Restores all accounts from an array of serialized accounts
|
|
142
|
+
* NOTE: this will clear all existing accounts
|
|
143
|
+
*/
|
|
144
|
+
fromJSON(accounts, quite = false) {
|
|
145
|
+
for (const json of accounts) {
|
|
146
|
+
try {
|
|
147
|
+
const AccountType = this.types.get(json.type);
|
|
148
|
+
if (!AccountType) {
|
|
149
|
+
if (!quite)
|
|
150
|
+
throw new Error(`Missing account type ${json.type}`);
|
|
151
|
+
else
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const account = AccountType.fromJSON(json);
|
|
155
|
+
this.addAccount(account);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (!quite)
|
|
159
|
+
throw error;
|
|
160
|
+
else
|
|
161
|
+
console.log(`Failed to load account`, error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
2
165
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { AccountManager } from "./manager.js";
|
|
3
|
+
import { SimpleAccount } from "./accounts/simple-account.js";
|
|
4
|
+
import { generateSecretKey, getPublicKey } from "nostr-tools";
|
|
5
|
+
import { bytesToHex } from "@noble/hashes/utils";
|
|
6
|
+
describe("AccountManager", () => {
|
|
7
|
+
let manager;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
manager = new AccountManager();
|
|
10
|
+
});
|
|
11
|
+
describe("toJSON", () => {
|
|
12
|
+
it("should return an array of serialized accounts", () => {
|
|
13
|
+
manager.addAccount(SimpleAccount.fromKey(generateSecretKey()));
|
|
14
|
+
manager.setAccountMetadata(manager.accounts[0], { name: "testing" });
|
|
15
|
+
expect(manager.toJSON()).toEqual([
|
|
16
|
+
{
|
|
17
|
+
id: expect.any(String),
|
|
18
|
+
type: "nsec",
|
|
19
|
+
pubkey: expect.any(String),
|
|
20
|
+
metadata: { name: "testing" },
|
|
21
|
+
signer: { key: expect.any(String) },
|
|
22
|
+
},
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
describe("fromJSON", () => {
|
|
27
|
+
it("should recreate accounts", () => {
|
|
28
|
+
const key = generateSecretKey();
|
|
29
|
+
const json = [
|
|
30
|
+
{
|
|
31
|
+
id: "custom-id",
|
|
32
|
+
type: "nsec",
|
|
33
|
+
pubkey: getPublicKey(key),
|
|
34
|
+
metadata: { name: "testing" },
|
|
35
|
+
signer: { key: bytesToHex(key) },
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
manager.registerType(SimpleAccount);
|
|
39
|
+
manager.fromJSON(json);
|
|
40
|
+
expect(manager.getAccount("custom-id")).toBeInstanceOf(SimpleAccount);
|
|
41
|
+
expect(manager.getAccountForPubkey(getPublicKey(key))).toBeInstanceOf(SimpleAccount);
|
|
42
|
+
expect(manager.getAccountMetadata("custom-id")).toEqual({ name: "testing" });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("signer", () => {
|
|
46
|
+
it("should proxy active account", async () => {
|
|
47
|
+
const account = SimpleAccount.generateNew();
|
|
48
|
+
manager.addAccount(account);
|
|
49
|
+
manager.setActive(account);
|
|
50
|
+
expect(await manager.signer.getPublicKey()).toBe(getPublicKey(account.signer.key));
|
|
51
|
+
});
|
|
52
|
+
it("should throw if there is no active account", () => {
|
|
53
|
+
expect(() => {
|
|
54
|
+
manager.signer.getPublicKey();
|
|
55
|
+
}).toThrow("No active account");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
package/dist/types.d.ts
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
|
-
import { Nip07Interface } from "applesauce-
|
|
2
|
-
export type
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { Nip07Interface } from "applesauce-signers";
|
|
2
|
+
export type EventTemplate = {
|
|
3
|
+
kind: number;
|
|
4
|
+
content: string;
|
|
5
|
+
tags: string[][];
|
|
6
|
+
created_at: number;
|
|
7
|
+
};
|
|
8
|
+
export type SerializedAccount<SignerData, Metadata extends unknown> = {
|
|
9
|
+
/** Internal account ID */
|
|
10
|
+
id: string;
|
|
11
|
+
/** account type */
|
|
12
|
+
type: string;
|
|
13
|
+
/** pubkey of the account */
|
|
5
14
|
pubkey: string;
|
|
6
|
-
|
|
15
|
+
/** Signer data */
|
|
16
|
+
signer: SignerData;
|
|
17
|
+
/** Extra application specific account metadata */
|
|
18
|
+
metadata?: Metadata;
|
|
7
19
|
};
|
|
8
|
-
export interface IAccount<
|
|
20
|
+
export interface IAccount<Signer extends Nip07Interface = Nip07Interface, SignerData = any, Metadata extends unknown = any> extends Nip07Interface {
|
|
21
|
+
id: string;
|
|
9
22
|
name?: string;
|
|
10
23
|
pubkey: string;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
metadata?: Metadata;
|
|
25
|
+
signer: Signer;
|
|
26
|
+
type: string;
|
|
27
|
+
disableQueue?: boolean;
|
|
28
|
+
toJSON(): SerializedAccount<SignerData, Metadata>;
|
|
15
29
|
}
|
|
16
|
-
export interface IAccountConstructor<
|
|
17
|
-
|
|
18
|
-
|
|
30
|
+
export interface IAccountConstructor<Signer extends Nip07Interface, SignerData, Metadata extends unknown> {
|
|
31
|
+
readonly type: string;
|
|
32
|
+
new (pubkey: string, signer: Signer): IAccount<Signer, SignerData, Metadata>;
|
|
33
|
+
fromJSON(json: SerializedAccount<SignerData, Metadata>): IAccount<Signer, SignerData, Metadata>;
|
|
19
34
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-accounts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "A simple nostr account management system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"keywords": [
|
|
9
|
-
"nostr"
|
|
9
|
+
"nostr",
|
|
10
|
+
"applesauce"
|
|
10
11
|
],
|
|
11
12
|
"author": "hzrd149",
|
|
12
13
|
"license": "MIT",
|
|
@@ -16,27 +17,30 @@
|
|
|
16
17
|
"exports": {
|
|
17
18
|
".": {
|
|
18
19
|
"import": "./dist/index.js",
|
|
20
|
+
"require": "./dist/index.js",
|
|
19
21
|
"types": "./dist/index.d.ts"
|
|
20
22
|
},
|
|
21
23
|
"./accounts": {
|
|
22
24
|
"import": "./dist/accounts/index.js",
|
|
25
|
+
"require": "./dist/accounts/index.js",
|
|
23
26
|
"types": "./dist/accounts/index.d.ts"
|
|
24
27
|
},
|
|
25
28
|
"./accounts/*": {
|
|
26
29
|
"import": "./dist/accounts/*.js",
|
|
30
|
+
"require": "./dist/accounts/*.js",
|
|
27
31
|
"types": "./dist/accounts/*.d.ts"
|
|
28
32
|
}
|
|
29
33
|
},
|
|
30
34
|
"dependencies": {
|
|
31
|
-
"@noble/hashes": "^1.
|
|
32
|
-
"applesauce-
|
|
35
|
+
"@noble/hashes": "^1.7.1",
|
|
36
|
+
"applesauce-signers": "^0.11.0",
|
|
33
37
|
"nanoid": "^5.0.9",
|
|
34
|
-
"nostr-tools": "^2.10.
|
|
38
|
+
"nostr-tools": "^2.10.4",
|
|
35
39
|
"rxjs": "^7.8.1"
|
|
36
40
|
},
|
|
37
41
|
"devDependencies": {
|
|
38
|
-
"typescript": "^5.
|
|
39
|
-
"vitest": "^
|
|
42
|
+
"typescript": "^5.7.3",
|
|
43
|
+
"vitest": "^3.0.5"
|
|
40
44
|
},
|
|
41
45
|
"funding": {
|
|
42
46
|
"type": "lightning",
|