@formstr/signer 0.1.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/LICENSE +21 -0
- package/README.md +275 -0
- package/dist/index.cjs +726 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +140 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.js +693 -0
- package/dist/index.js.map +1 -0
- package/dist/signer-DiKj4PR7.d.cts +377 -0
- package/dist/signer-DiKj4PR7.d.ts +377 -0
- package/dist/ui/index.cjs +334 -0
- package/dist/ui/index.cjs.map +1 -0
- package/dist/ui/index.d.cts +24 -0
- package/dist/ui/index.d.ts +24 -0
- package/dist/ui/index.js +298 -0
- package/dist/ui/index.js.map +1 -0
- package/package.json +85 -0
- package/styles/signer.css +263 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
AndroidSigner: () => AndroidSigner,
|
|
24
|
+
BunkerSigner: () => BunkerSigner,
|
|
25
|
+
ExtensionSigner: () => ExtensionSigner,
|
|
26
|
+
LocalSigner: () => LocalSigner,
|
|
27
|
+
Signer: () => Signer,
|
|
28
|
+
bytesToHex: () => bytesToHex,
|
|
29
|
+
connectWithBunkerUri: () => connectWithBunkerUri,
|
|
30
|
+
createSigner: () => createSigner,
|
|
31
|
+
decryptNcryptsec: () => decryptNcryptsec,
|
|
32
|
+
encryptSecretKey: () => encryptSecretKey,
|
|
33
|
+
generateAccount: () => generateAccount,
|
|
34
|
+
getWindowNostr: () => getWindowNostr,
|
|
35
|
+
hexToBytes: () => hexToBytes,
|
|
36
|
+
initiateNostrConnect: () => initiateNostrConnect,
|
|
37
|
+
localStorageAdapter: () => localStorageAdapter,
|
|
38
|
+
loginWithAndroidSigner: () => loginWithAndroidSigner
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(src_exports);
|
|
41
|
+
|
|
42
|
+
// src/core/signer.ts
|
|
43
|
+
var import_nostr_tools5 = require("nostr-tools");
|
|
44
|
+
|
|
45
|
+
// src/core/storage.ts
|
|
46
|
+
var DEFAULT_PREFIX = "@formstr/signer:";
|
|
47
|
+
function localStorageAdapter(prefix = DEFAULT_PREFIX) {
|
|
48
|
+
const ls = () => {
|
|
49
|
+
try {
|
|
50
|
+
return typeof globalThis !== "undefined" && globalThis.localStorage ? globalThis.localStorage : null;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
get(key) {
|
|
57
|
+
try {
|
|
58
|
+
return ls()?.getItem(prefix + key) ?? null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
set(key, value) {
|
|
64
|
+
try {
|
|
65
|
+
ls()?.setItem(prefix + key, value);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
remove(key) {
|
|
70
|
+
try {
|
|
71
|
+
ls()?.removeItem(prefix + key);
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/core/localSigner.ts
|
|
79
|
+
var import_nostr_tools = require("nostr-tools");
|
|
80
|
+
var LocalSigner = class {
|
|
81
|
+
#secretKey;
|
|
82
|
+
constructor(secretKey) {
|
|
83
|
+
this.#secretKey = secretKey;
|
|
84
|
+
}
|
|
85
|
+
async getPublicKey() {
|
|
86
|
+
return (0, import_nostr_tools.getPublicKey)(this.#secretKey);
|
|
87
|
+
}
|
|
88
|
+
async signEvent(event) {
|
|
89
|
+
return (0, import_nostr_tools.finalizeEvent)(event, this.#secretKey);
|
|
90
|
+
}
|
|
91
|
+
async nip04Encrypt(peerPubkey, plaintext) {
|
|
92
|
+
return import_nostr_tools.nip04.encrypt(this.#secretKey, peerPubkey, plaintext);
|
|
93
|
+
}
|
|
94
|
+
async nip04Decrypt(peerPubkey, ciphertext) {
|
|
95
|
+
return import_nostr_tools.nip04.decrypt(this.#secretKey, peerPubkey, ciphertext);
|
|
96
|
+
}
|
|
97
|
+
async nip44Encrypt(peerPubkey, plaintext) {
|
|
98
|
+
const key = import_nostr_tools.nip44.v2.utils.getConversationKey(this.#secretKey, peerPubkey);
|
|
99
|
+
return import_nostr_tools.nip44.v2.encrypt(plaintext, key);
|
|
100
|
+
}
|
|
101
|
+
async nip44Decrypt(peerPubkey, ciphertext) {
|
|
102
|
+
const key = import_nostr_tools.nip44.v2.utils.getConversationKey(this.#secretKey, peerPubkey);
|
|
103
|
+
return import_nostr_tools.nip44.v2.decrypt(ciphertext, key);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// src/nip49.ts
|
|
108
|
+
var import_nostr_tools2 = require("nostr-tools");
|
|
109
|
+
var import_nip49 = require("nostr-tools/nip49");
|
|
110
|
+
function encryptSecretKey(secretKey, passphrase) {
|
|
111
|
+
return (0, import_nip49.encrypt)(secretKey, passphrase);
|
|
112
|
+
}
|
|
113
|
+
function decryptNcryptsec(ncryptsec, passphrase) {
|
|
114
|
+
return (0, import_nip49.decrypt)(ncryptsec, passphrase);
|
|
115
|
+
}
|
|
116
|
+
function generateAccount(passphrase) {
|
|
117
|
+
const secretKey = (0, import_nostr_tools2.generateSecretKey)();
|
|
118
|
+
const pubkey = (0, import_nostr_tools2.getPublicKey)(secretKey);
|
|
119
|
+
const npub = import_nostr_tools2.nip19.npubEncode(pubkey);
|
|
120
|
+
const ncryptsec = (0, import_nip49.encrypt)(secretKey, passphrase);
|
|
121
|
+
return { secretKey, pubkey, npub, ncryptsec };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/nip07.ts
|
|
125
|
+
function getWindowNostr() {
|
|
126
|
+
const nostr = globalThis.nostr;
|
|
127
|
+
if (!nostr) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
"@formstr/signer: NIP-07 extension not found (globalThis.nostr is undefined)"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return nostr;
|
|
133
|
+
}
|
|
134
|
+
var ExtensionSigner = class {
|
|
135
|
+
async getPublicKey() {
|
|
136
|
+
return getWindowNostr().getPublicKey();
|
|
137
|
+
}
|
|
138
|
+
async signEvent(event) {
|
|
139
|
+
return getWindowNostr().signEvent(event);
|
|
140
|
+
}
|
|
141
|
+
async nip04Encrypt(peerPubkey, plaintext) {
|
|
142
|
+
const ext = getWindowNostr();
|
|
143
|
+
if (!ext.nip04) throw new Error("NIP-07 extension does not expose nip04");
|
|
144
|
+
return ext.nip04.encrypt(peerPubkey, plaintext);
|
|
145
|
+
}
|
|
146
|
+
async nip04Decrypt(peerPubkey, ciphertext) {
|
|
147
|
+
const ext = getWindowNostr();
|
|
148
|
+
if (!ext.nip04) throw new Error("NIP-07 extension does not expose nip04");
|
|
149
|
+
return ext.nip04.decrypt(peerPubkey, ciphertext);
|
|
150
|
+
}
|
|
151
|
+
async nip44Encrypt(peerPubkey, plaintext) {
|
|
152
|
+
const ext = getWindowNostr();
|
|
153
|
+
if (!ext.nip44) throw new Error("NIP-07 extension does not expose nip44");
|
|
154
|
+
return ext.nip44.encrypt(peerPubkey, plaintext);
|
|
155
|
+
}
|
|
156
|
+
async nip44Decrypt(peerPubkey, ciphertext) {
|
|
157
|
+
const ext = getWindowNostr();
|
|
158
|
+
if (!ext.nip44) throw new Error("NIP-07 extension does not expose nip44");
|
|
159
|
+
return ext.nip44.decrypt(peerPubkey, ciphertext);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// src/nip46.ts
|
|
164
|
+
var import_nostr_tools3 = require("nostr-tools");
|
|
165
|
+
var import_nip46 = require("nostr-tools/nip46");
|
|
166
|
+
var BunkerSigner = class {
|
|
167
|
+
#delegate;
|
|
168
|
+
constructor(delegate) {
|
|
169
|
+
this.#delegate = delegate;
|
|
170
|
+
}
|
|
171
|
+
getPublicKey() {
|
|
172
|
+
return this.#delegate.getPublicKey();
|
|
173
|
+
}
|
|
174
|
+
signEvent(event) {
|
|
175
|
+
return this.#delegate.signEvent(event);
|
|
176
|
+
}
|
|
177
|
+
nip04Encrypt(peerPubkey, plaintext) {
|
|
178
|
+
return this.#delegate.nip04Encrypt(peerPubkey, plaintext);
|
|
179
|
+
}
|
|
180
|
+
nip04Decrypt(peerPubkey, ciphertext) {
|
|
181
|
+
return this.#delegate.nip04Decrypt(peerPubkey, ciphertext);
|
|
182
|
+
}
|
|
183
|
+
nip44Encrypt(peerPubkey, plaintext) {
|
|
184
|
+
return this.#delegate.nip44Encrypt(peerPubkey, plaintext);
|
|
185
|
+
}
|
|
186
|
+
nip44Decrypt(peerPubkey, ciphertext) {
|
|
187
|
+
return this.#delegate.nip44Decrypt(peerPubkey, ciphertext);
|
|
188
|
+
}
|
|
189
|
+
async close() {
|
|
190
|
+
return this.#delegate.close();
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
async function fetchBunkerRelays(tools) {
|
|
194
|
+
try {
|
|
195
|
+
const resp = await tools.sendRequest("get_relays", []);
|
|
196
|
+
const parsed = JSON.parse(resp);
|
|
197
|
+
if (Array.isArray(parsed)) {
|
|
198
|
+
return parsed.filter((r) => typeof r === "string");
|
|
199
|
+
}
|
|
200
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
201
|
+
return Object.keys(parsed);
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function relayListsMatch(a, b) {
|
|
209
|
+
if (a.length !== b.length) return false;
|
|
210
|
+
const sa = [...a].sort();
|
|
211
|
+
const sb = [...b].sort();
|
|
212
|
+
for (let i = 0; i < sa.length; i++) if (sa[i] !== sb[i]) return false;
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
async function resolveRelayChoice(tools, userRelays, onRelayMismatch) {
|
|
216
|
+
if (!onRelayMismatch) return userRelays;
|
|
217
|
+
const bunkerRelays = await fetchBunkerRelays(tools);
|
|
218
|
+
if (!bunkerRelays || relayListsMatch(userRelays, bunkerRelays)) return userRelays;
|
|
219
|
+
const accept = await onRelayMismatch({ userRelays, bunkerRelays });
|
|
220
|
+
return accept ? bunkerRelays : userRelays;
|
|
221
|
+
}
|
|
222
|
+
async function connectWithBunkerUri(uri, options = {}) {
|
|
223
|
+
const pointer = await (0, import_nip46.parseBunkerInput)(uri);
|
|
224
|
+
if (!pointer) {
|
|
225
|
+
throw new Error("@formstr/signer: invalid bunker URI");
|
|
226
|
+
}
|
|
227
|
+
if (!pointer.relays?.length) {
|
|
228
|
+
throw new Error("@formstr/signer: bunker URI must include at least one relay");
|
|
229
|
+
}
|
|
230
|
+
const clientSecretKey = options.clientSecretKey ?? (0, import_nostr_tools3.generateSecretKey)();
|
|
231
|
+
const tools = import_nip46.BunkerSigner.fromBunker(clientSecretKey, pointer, {
|
|
232
|
+
pool: options.pool,
|
|
233
|
+
onauth: options.onAuth
|
|
234
|
+
});
|
|
235
|
+
await tools.sendRequest("connect", [
|
|
236
|
+
pointer.pubkey,
|
|
237
|
+
pointer.secret ?? "",
|
|
238
|
+
(options.perms ?? []).join(",")
|
|
239
|
+
]);
|
|
240
|
+
const pubkey = await tools.getPublicKey();
|
|
241
|
+
const resolvedRelays = await resolveRelayChoice(
|
|
242
|
+
tools,
|
|
243
|
+
pointer.relays,
|
|
244
|
+
options.onRelayMismatch
|
|
245
|
+
);
|
|
246
|
+
return {
|
|
247
|
+
signer: new BunkerSigner(tools),
|
|
248
|
+
pubkey,
|
|
249
|
+
pointer: { ...pointer, relays: resolvedRelays },
|
|
250
|
+
clientSecretKey
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function initiateNostrConnect(options) {
|
|
254
|
+
if (options.relays.length === 0) {
|
|
255
|
+
throw new Error("@formstr/signer: at least one relay is required for nostrconnect");
|
|
256
|
+
}
|
|
257
|
+
const clientSecretKey = options.clientSecretKey ?? (0, import_nostr_tools3.generateSecretKey)();
|
|
258
|
+
const clientPubkey = (0, import_nostr_tools3.getPublicKey)(clientSecretKey);
|
|
259
|
+
const secret = options.secret ?? Math.random().toString(36).slice(2);
|
|
260
|
+
const uri = (0, import_nip46.createNostrConnectURI)({
|
|
261
|
+
clientPubkey,
|
|
262
|
+
relays: options.relays,
|
|
263
|
+
secret,
|
|
264
|
+
perms: options.perms,
|
|
265
|
+
name: options.metadata?.name,
|
|
266
|
+
url: options.metadata?.url,
|
|
267
|
+
image: options.metadata?.image
|
|
268
|
+
});
|
|
269
|
+
const maxWaitOrAbort = options.signal ?? options.timeoutMs ?? 3e5;
|
|
270
|
+
const complete = import_nip46.BunkerSigner.fromURI(
|
|
271
|
+
clientSecretKey,
|
|
272
|
+
uri,
|
|
273
|
+
{ pool: options.pool, onauth: options.onAuth, skipSwitchRelays: true },
|
|
274
|
+
maxWaitOrAbort
|
|
275
|
+
).then(async (tools) => {
|
|
276
|
+
const pubkey = await tools.getPublicKey();
|
|
277
|
+
const resolvedRelays = await resolveRelayChoice(
|
|
278
|
+
tools,
|
|
279
|
+
options.relays,
|
|
280
|
+
options.onRelayMismatch
|
|
281
|
+
);
|
|
282
|
+
return {
|
|
283
|
+
signer: new BunkerSigner(tools),
|
|
284
|
+
pubkey,
|
|
285
|
+
pointer: { ...tools.bp, relays: resolvedRelays },
|
|
286
|
+
clientSecretKey
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
return { uri, clientPubkey, complete };
|
|
290
|
+
}
|
|
291
|
+
var hexAlphabet = "0123456789abcdef";
|
|
292
|
+
function bytesToHex(bytes) {
|
|
293
|
+
let s = "";
|
|
294
|
+
for (const b of bytes) s += hexAlphabet[b >> 4] + hexAlphabet[b & 15];
|
|
295
|
+
return s;
|
|
296
|
+
}
|
|
297
|
+
function hexToBytes(hex) {
|
|
298
|
+
if (hex.length % 2 !== 0) throw new Error("hexToBytes: odd-length hex string");
|
|
299
|
+
const out = new Uint8Array(hex.length / 2);
|
|
300
|
+
for (let i = 0; i < out.length; i++) {
|
|
301
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/nip55.ts
|
|
307
|
+
var import_nostr_tools4 = require("nostr-tools");
|
|
308
|
+
var AndroidSigner = class {
|
|
309
|
+
#plugin;
|
|
310
|
+
#packageName;
|
|
311
|
+
#npub;
|
|
312
|
+
#pubkey;
|
|
313
|
+
constructor(plugin, packageName, npub, pubkey) {
|
|
314
|
+
this.#plugin = plugin;
|
|
315
|
+
this.#packageName = packageName;
|
|
316
|
+
this.#npub = npub;
|
|
317
|
+
this.#pubkey = pubkey;
|
|
318
|
+
}
|
|
319
|
+
async getPublicKey() {
|
|
320
|
+
return this.#pubkey;
|
|
321
|
+
}
|
|
322
|
+
async signEvent(event) {
|
|
323
|
+
const unsigned = { ...event, pubkey: this.#pubkey };
|
|
324
|
+
const eventId = (0, import_nostr_tools4.getEventHash)(unsigned);
|
|
325
|
+
const result = await this.#plugin.signEvent(
|
|
326
|
+
this.#packageName,
|
|
327
|
+
JSON.stringify(unsigned),
|
|
328
|
+
eventId,
|
|
329
|
+
this.#npub
|
|
330
|
+
);
|
|
331
|
+
return JSON.parse(result.event);
|
|
332
|
+
}
|
|
333
|
+
async nip04Encrypt(peerPubkey, plaintext) {
|
|
334
|
+
const { result } = await this.#plugin.nip04Encrypt(
|
|
335
|
+
this.#packageName,
|
|
336
|
+
plaintext,
|
|
337
|
+
"",
|
|
338
|
+
peerPubkey,
|
|
339
|
+
this.#npub
|
|
340
|
+
);
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
async nip04Decrypt(peerPubkey, ciphertext) {
|
|
344
|
+
const { result } = await this.#plugin.nip04Decrypt(
|
|
345
|
+
this.#packageName,
|
|
346
|
+
ciphertext,
|
|
347
|
+
"",
|
|
348
|
+
peerPubkey,
|
|
349
|
+
this.#npub
|
|
350
|
+
);
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
async nip44Encrypt(peerPubkey, plaintext) {
|
|
354
|
+
const { result } = await this.#plugin.nip44Encrypt(
|
|
355
|
+
this.#packageName,
|
|
356
|
+
plaintext,
|
|
357
|
+
"",
|
|
358
|
+
peerPubkey,
|
|
359
|
+
this.#npub
|
|
360
|
+
);
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
async nip44Decrypt(peerPubkey, ciphertext) {
|
|
364
|
+
const { result } = await this.#plugin.nip44Decrypt(
|
|
365
|
+
this.#packageName,
|
|
366
|
+
ciphertext,
|
|
367
|
+
"",
|
|
368
|
+
peerPubkey,
|
|
369
|
+
this.#npub
|
|
370
|
+
);
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
async function loginWithAndroidSigner(plugin, packageName) {
|
|
375
|
+
if (packageName) {
|
|
376
|
+
await plugin.setPackageName(packageName);
|
|
377
|
+
}
|
|
378
|
+
const { npub, package: pluginPackage } = await plugin.getPublicKey(packageName);
|
|
379
|
+
const resolvedPackage = pluginPackage || packageName;
|
|
380
|
+
if (!resolvedPackage) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
"@formstr/signer: android signer did not return a package name and none was supplied"
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
const decoded = import_nostr_tools4.nip19.decode(npub);
|
|
386
|
+
if (decoded.type !== "npub") {
|
|
387
|
+
throw new Error("@formstr/signer: android signer returned a non-npub identifier");
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
signer: new AndroidSigner(plugin, resolvedPackage, npub, decoded.data),
|
|
391
|
+
pubkey: decoded.data,
|
|
392
|
+
npub,
|
|
393
|
+
packageName: resolvedPackage
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/core/signer.ts
|
|
398
|
+
var ACCOUNTS_KEY = "accounts";
|
|
399
|
+
var ACTIVE_KEY = "active-pubkey";
|
|
400
|
+
var Signer = class {
|
|
401
|
+
#storage;
|
|
402
|
+
#defaultAndroidPlugin;
|
|
403
|
+
#appMetadata;
|
|
404
|
+
#accounts = [];
|
|
405
|
+
#activePubkey = null;
|
|
406
|
+
#activeSigner = null;
|
|
407
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
408
|
+
constructor(config = {}) {
|
|
409
|
+
this.#storage = config.storage ?? localStorageAdapter(config.storageKeyPrefix);
|
|
410
|
+
this.#defaultAndroidPlugin = config.androidSignerPlugin;
|
|
411
|
+
this.#appMetadata = {
|
|
412
|
+
name: config.appName,
|
|
413
|
+
url: config.appUrl,
|
|
414
|
+
image: config.appImage
|
|
415
|
+
};
|
|
416
|
+
this.#hydrate();
|
|
417
|
+
}
|
|
418
|
+
#hydrate() {
|
|
419
|
+
try {
|
|
420
|
+
const raw = this.#storage.get(ACCOUNTS_KEY);
|
|
421
|
+
if (raw) {
|
|
422
|
+
const parsed = JSON.parse(raw);
|
|
423
|
+
if (Array.isArray(parsed)) this.#accounts = parsed;
|
|
424
|
+
}
|
|
425
|
+
this.#activePubkey = this.#storage.get(ACTIVE_KEY);
|
|
426
|
+
} catch {
|
|
427
|
+
this.#accounts = [];
|
|
428
|
+
this.#activePubkey = null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
#persistAccounts() {
|
|
432
|
+
this.#storage.set(ACCOUNTS_KEY, JSON.stringify(this.#accounts));
|
|
433
|
+
}
|
|
434
|
+
#persistActive() {
|
|
435
|
+
if (this.#activePubkey) this.#storage.set(ACTIVE_KEY, this.#activePubkey);
|
|
436
|
+
else this.#storage.remove(ACTIVE_KEY);
|
|
437
|
+
}
|
|
438
|
+
#upsertAccount(account) {
|
|
439
|
+
const idx = this.#accounts.findIndex((a) => a.pubkey === account.pubkey);
|
|
440
|
+
if (idx >= 0) this.#accounts[idx] = account;
|
|
441
|
+
else this.#accounts.push(account);
|
|
442
|
+
this.#persistAccounts();
|
|
443
|
+
}
|
|
444
|
+
#setActive(account, signer) {
|
|
445
|
+
const wasDifferent = this.#activePubkey !== null && this.#activePubkey !== account.pubkey;
|
|
446
|
+
this.#activePubkey = account.pubkey;
|
|
447
|
+
this.#activeSigner = signer;
|
|
448
|
+
this.#persistActive();
|
|
449
|
+
this.#emit({ type: wasDifferent ? "switch" : "login", account });
|
|
450
|
+
}
|
|
451
|
+
#emit(event) {
|
|
452
|
+
for (const cb of this.#listeners) {
|
|
453
|
+
try {
|
|
454
|
+
cb(event);
|
|
455
|
+
} catch {
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Generate a brand-new nsec, encrypt it with `passphrase` (NIP-49),
|
|
461
|
+
* persist the resulting `ncryptsec` account, and activate it. Returns
|
|
462
|
+
* the new account's `npub` and `ncryptsec` — the caller must surface
|
|
463
|
+
* the `ncryptsec` to the user **immediately** since it is the only way
|
|
464
|
+
* back into the account on a fresh device.
|
|
465
|
+
*
|
|
466
|
+
* @throws if `passphrase` is empty.
|
|
467
|
+
*/
|
|
468
|
+
async createAccount(passphrase) {
|
|
469
|
+
if (!passphrase) throw new Error("createAccount: passphrase required");
|
|
470
|
+
const { secretKey, pubkey, npub, ncryptsec } = generateAccount(passphrase);
|
|
471
|
+
const account = { npub, pubkey, method: "ncryptsec", ncryptsec };
|
|
472
|
+
this.#upsertAccount(account);
|
|
473
|
+
this.#setActive(account, new LocalSigner(secretKey));
|
|
474
|
+
return { npub, ncryptsec };
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Decrypt an ncryptsec with the user's passphrase, persist the account
|
|
478
|
+
* (overwriting any previous entry for the same pubkey), and activate it.
|
|
479
|
+
*
|
|
480
|
+
* @throws if either argument is empty, or if the passphrase doesn't
|
|
481
|
+
* decrypt the ncryptsec.
|
|
482
|
+
*/
|
|
483
|
+
async loginWithNcryptsec(ncryptsec, passphrase) {
|
|
484
|
+
if (!ncryptsec) throw new Error("loginWithNcryptsec: ncryptsec required");
|
|
485
|
+
if (!passphrase) throw new Error("loginWithNcryptsec: passphrase required");
|
|
486
|
+
const secretKey = decryptNcryptsec(ncryptsec, passphrase);
|
|
487
|
+
const pubkey = (0, import_nostr_tools5.getPublicKey)(secretKey);
|
|
488
|
+
const npub = import_nostr_tools5.nip19.npubEncode(pubkey);
|
|
489
|
+
const account = { npub, pubkey, method: "ncryptsec", ncryptsec };
|
|
490
|
+
this.#upsertAccount(account);
|
|
491
|
+
this.#setActive(account, new LocalSigner(secretKey));
|
|
492
|
+
return account;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Connect via the NIP-07 browser extension exposed at `window.nostr`.
|
|
496
|
+
* The extension prompts the user for permission on first use.
|
|
497
|
+
*
|
|
498
|
+
* @throws if no extension is installed or the user denies the request.
|
|
499
|
+
*/
|
|
500
|
+
async loginWithExtension() {
|
|
501
|
+
const extension = new ExtensionSigner();
|
|
502
|
+
const pubkey = await extension.getPublicKey();
|
|
503
|
+
const npub = import_nostr_tools5.nip19.npubEncode(pubkey);
|
|
504
|
+
const account = { npub, pubkey, method: "extension" };
|
|
505
|
+
this.#upsertAccount(account);
|
|
506
|
+
this.#setActive(account, extension);
|
|
507
|
+
return account;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Connect to a NIP-46 remote signer via a `bunker://` URI. Relays are
|
|
511
|
+
* read from the URI itself — no hardcoded fallbacks. Pass a `pool`
|
|
512
|
+
* to reuse an existing relay connection; pass `clientSecretKey` to
|
|
513
|
+
* resume a previous session (the hex from `StoredAccount.nip46`).
|
|
514
|
+
*
|
|
515
|
+
* @throws if the URI is malformed, no relay is reachable, or the
|
|
516
|
+
* remote signer rejects pairing within the implementation's timeout.
|
|
517
|
+
*/
|
|
518
|
+
async loginWithBunkerUri(uri, options = {}) {
|
|
519
|
+
const result = await connectWithBunkerUri(uri, options);
|
|
520
|
+
const npub = import_nostr_tools5.nip19.npubEncode(result.pubkey);
|
|
521
|
+
const account = {
|
|
522
|
+
npub,
|
|
523
|
+
pubkey: result.pubkey,
|
|
524
|
+
method: "nip46",
|
|
525
|
+
nip46: {
|
|
526
|
+
uri,
|
|
527
|
+
remoteSignerPubkey: result.pointer.pubkey,
|
|
528
|
+
relays: result.pointer.relays,
|
|
529
|
+
clientSecretKey: bytesToHex(result.clientSecretKey)
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
this.#upsertAccount(account);
|
|
533
|
+
this.#setActive(account, result.signer);
|
|
534
|
+
return account;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Initiate a NIP-46 `nostrconnect://` pairing. Generates a client
|
|
538
|
+
* keypair, publishes a connect request to the supplied `relays`, and
|
|
539
|
+
* waits for a remote signer to pair. Call `options.onUri(uri)` to
|
|
540
|
+
* render the URI as a QR code; the returned promise resolves once
|
|
541
|
+
* pairing completes. Cancel by aborting `options.signal`.
|
|
542
|
+
*
|
|
543
|
+
* @throws if `relays` is empty, the user aborts, the pairing times
|
|
544
|
+
* out, or no signer responds.
|
|
545
|
+
*/
|
|
546
|
+
async loginWithNostrConnect(options) {
|
|
547
|
+
if (options.relays.length === 0) {
|
|
548
|
+
throw new Error("loginWithNostrConnect: at least one relay required");
|
|
549
|
+
}
|
|
550
|
+
const metadata = {
|
|
551
|
+
name: options.metadata?.name ?? this.#appMetadata.name,
|
|
552
|
+
url: options.metadata?.url ?? this.#appMetadata.url,
|
|
553
|
+
image: options.metadata?.image ?? this.#appMetadata.image
|
|
554
|
+
};
|
|
555
|
+
if (!metadata.name) {
|
|
556
|
+
throw new Error(
|
|
557
|
+
"@formstr/signer: loginWithNostrConnect requires an app name. Set `appName` in createSigner() or pass `metadata.name` to loginWithNostrConnect()."
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
const init = initiateNostrConnect({
|
|
561
|
+
relays: options.relays,
|
|
562
|
+
metadata,
|
|
563
|
+
perms: options.perms,
|
|
564
|
+
pool: options.pool,
|
|
565
|
+
onAuth: options.onAuth,
|
|
566
|
+
signal: options.signal,
|
|
567
|
+
timeoutMs: options.timeoutMs,
|
|
568
|
+
onRelayMismatch: options.onRelayMismatch
|
|
569
|
+
});
|
|
570
|
+
options.onUri(init.uri);
|
|
571
|
+
const result = await init.complete;
|
|
572
|
+
const npub = import_nostr_tools5.nip19.npubEncode(result.pubkey);
|
|
573
|
+
const account = {
|
|
574
|
+
npub,
|
|
575
|
+
pubkey: result.pubkey,
|
|
576
|
+
method: "nip46",
|
|
577
|
+
nip46: {
|
|
578
|
+
uri: init.uri,
|
|
579
|
+
remoteSignerPubkey: result.pointer.pubkey,
|
|
580
|
+
relays: result.pointer.relays,
|
|
581
|
+
clientSecretKey: bytesToHex(result.clientSecretKey)
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
this.#upsertAccount(account);
|
|
585
|
+
this.#setActive(account, result.signer);
|
|
586
|
+
return account;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Enumerate NIP-55 signer apps installed on the device, via the
|
|
590
|
+
* configured Android plugin (or `plugin` if supplied). Useful for
|
|
591
|
+
* rendering a "pick your signer" list — the built-in UI does this
|
|
592
|
+
* automatically when the Android tab is selected.
|
|
593
|
+
*
|
|
594
|
+
* Only meaningful inside a Capacitor Android shell. On web/iOS the
|
|
595
|
+
* configured plugin is typically absent and this throws.
|
|
596
|
+
*
|
|
597
|
+
* @throws if no plugin is configured and none is passed in.
|
|
598
|
+
*/
|
|
599
|
+
async listAndroidSignerApps(plugin) {
|
|
600
|
+
const p = plugin ?? this.#defaultAndroidPlugin;
|
|
601
|
+
if (!p) {
|
|
602
|
+
throw new Error(
|
|
603
|
+
"@formstr/signer: no Android signer plugin configured (pass `androidSignerPlugin` to createSigner or `plugin` to listAndroidSignerApps)"
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
const { apps } = await p.getInstalledSignerApps();
|
|
607
|
+
return apps;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Sign in via a NIP-55 Android external signer (Amber, etc). If
|
|
611
|
+
* `options.packageName` is given, that specific signer app is invoked;
|
|
612
|
+
* otherwise the plugin picks a default (typically the only installed
|
|
613
|
+
* signer, or an OS chooser). Pass `options.plugin` to override the
|
|
614
|
+
* configured default for this call.
|
|
615
|
+
*
|
|
616
|
+
* @throws if no plugin is configured, the signer app cannot be
|
|
617
|
+
* resolved to a package name, or the user denies the request.
|
|
618
|
+
*/
|
|
619
|
+
async loginWithAndroidSigner(options = {}) {
|
|
620
|
+
const plugin = options.plugin ?? this.#defaultAndroidPlugin;
|
|
621
|
+
if (!plugin) {
|
|
622
|
+
throw new Error(
|
|
623
|
+
"@formstr/signer: no Android signer plugin configured (pass `androidSignerPlugin` to createSigner or `plugin` to loginWithAndroidSigner)"
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
const result = await loginWithAndroidSigner(plugin, options.packageName);
|
|
627
|
+
const account = {
|
|
628
|
+
npub: result.npub,
|
|
629
|
+
pubkey: result.pubkey,
|
|
630
|
+
method: "android",
|
|
631
|
+
androidPackageName: result.packageName
|
|
632
|
+
};
|
|
633
|
+
this.#upsertAccount(account);
|
|
634
|
+
this.#setActive(account, result.signer);
|
|
635
|
+
return account;
|
|
636
|
+
}
|
|
637
|
+
/** Snapshot of every persisted account, in insertion order. */
|
|
638
|
+
listAccounts() {
|
|
639
|
+
return [...this.#accounts];
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* The currently selected account, or `null` if none. Present even when
|
|
643
|
+
* the account is locked (no active signer yet). Use this to render
|
|
644
|
+
* "logged in as @alice" — pair with {@link getActiveSigner} to decide
|
|
645
|
+
* whether signing is actually available.
|
|
646
|
+
*/
|
|
647
|
+
getActiveAccount() {
|
|
648
|
+
if (!this.#activePubkey) return null;
|
|
649
|
+
return this.#accounts.find((a) => a.pubkey === this.#activePubkey) ?? null;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* The unlocked signer for the active account, or `null` if locked.
|
|
653
|
+
* After a fresh page load this is `null` for every account type
|
|
654
|
+
* (passphrase / extension grant / signer-app handshake all need to
|
|
655
|
+
* be redone). Calling the matching `loginWith*` method unlocks it.
|
|
656
|
+
*/
|
|
657
|
+
getActiveSigner() {
|
|
658
|
+
return this.#activeSigner;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Make `pubkey` the active account. Clears the in-memory signer —
|
|
662
|
+
* the new account starts **locked** even if it was previously
|
|
663
|
+
* unlocked in this session.
|
|
664
|
+
*
|
|
665
|
+
* @throws if `pubkey` does not match any persisted account.
|
|
666
|
+
*/
|
|
667
|
+
async switchAccount(pubkey) {
|
|
668
|
+
const account = this.#accounts.find((a) => a.pubkey === pubkey);
|
|
669
|
+
if (!account) throw new Error(`switchAccount: no account for pubkey ${pubkey}`);
|
|
670
|
+
this.#activePubkey = pubkey;
|
|
671
|
+
this.#activeSigner = null;
|
|
672
|
+
this.#persistActive();
|
|
673
|
+
this.#emit({ type: "switch", account });
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Remove an account from storage. `pubkey` defaults to the active
|
|
677
|
+
* account. If the active account is removed, the in-memory signer is
|
|
678
|
+
* cleared. No-op if there is nothing to remove.
|
|
679
|
+
*/
|
|
680
|
+
async logout(pubkey) {
|
|
681
|
+
const target = pubkey ?? this.#activePubkey;
|
|
682
|
+
if (!target) return;
|
|
683
|
+
this.#accounts = this.#accounts.filter((a) => a.pubkey !== target);
|
|
684
|
+
this.#persistAccounts();
|
|
685
|
+
if (this.#activePubkey === target) {
|
|
686
|
+
this.#activePubkey = null;
|
|
687
|
+
this.#activeSigner = null;
|
|
688
|
+
this.#persistActive();
|
|
689
|
+
}
|
|
690
|
+
this.#emit({ type: "logout", pubkey: target });
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Subscribe to account-state changes. Returns an unsubscribe function.
|
|
694
|
+
* Listener errors are swallowed so one bad listener can't break others.
|
|
695
|
+
* See {@link SignerEvent} for the variants.
|
|
696
|
+
*/
|
|
697
|
+
onChange(cb) {
|
|
698
|
+
this.#listeners.add(cb);
|
|
699
|
+
return () => {
|
|
700
|
+
this.#listeners.delete(cb);
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
function createSigner(config = {}) {
|
|
705
|
+
return new Signer(config);
|
|
706
|
+
}
|
|
707
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
708
|
+
0 && (module.exports = {
|
|
709
|
+
AndroidSigner,
|
|
710
|
+
BunkerSigner,
|
|
711
|
+
ExtensionSigner,
|
|
712
|
+
LocalSigner,
|
|
713
|
+
Signer,
|
|
714
|
+
bytesToHex,
|
|
715
|
+
connectWithBunkerUri,
|
|
716
|
+
createSigner,
|
|
717
|
+
decryptNcryptsec,
|
|
718
|
+
encryptSecretKey,
|
|
719
|
+
generateAccount,
|
|
720
|
+
getWindowNostr,
|
|
721
|
+
hexToBytes,
|
|
722
|
+
initiateNostrConnect,
|
|
723
|
+
localStorageAdapter,
|
|
724
|
+
loginWithAndroidSigner
|
|
725
|
+
});
|
|
726
|
+
//# sourceMappingURL=index.cjs.map
|