Tools for developing Encrypted Nostr Clients. Depends on @scure & @noble packages.
npm install defy.tools # or yarn add defy.tools
For a demonstration of a secure messaging client using this library see https://gitlab.com/d5i/defy.demo .
Using web storage:
const keyStore = KeyStoreStorage.create(KeyStoreWebStorage);
// without a password the key will be saved in sessionStorage
// encrypted with a randomly generated key this will be
// available for 15 minutes.
await keyStore.save();
// with a password, the key will be saved in localStorage
// encrypted with a hash of the password
await keyStore.save("A Demo Password");
Using file storage:
// uses file a file located at $HOME/.defy/defy-backup to store the key
const keyStore = KeyStoreStorage.create(KeyStoreFileStorage);
await keyStore.save("A Demo Password");
// reload from session
const keyStoreA = await KeyStoreStorage.fromStorage(KeyStoreWebStorage);
// reload from local
const keyStoreB = await KeyStoreStorage.fromStorage(KeyStoreWebStorage, "A Demo Password");
// reload from file
const keyStoreC = await KeyStoreStorage.fromStorage(KeyStoreFileStorage, "A Demo Password");
Use RelayPool
after setting it with the setDefyContext()
method.
const keyStore = await KeyStoreStorage.fromStorage(KeyStoreFileStorage);
const relayPool = new RelayPool(); // or any class that extends RelayPool
relayPool.addRelay('wss://my.relay.url', true, true, false);
// relay methods require a context. for now we are assuming
// there is only one context per process.
setDefyContext({ keyStore, relayPool });
keyStore.load().subscribe(keyStore => {
// do stuff with keyStore.channels / keyStore.contacts
})
All methods in the model with names like load
, query
, publish
, listen
and delete
(and others) require a relayPool
object set in the context.
Same as above, but the last parameter to addRelay
is true
and we have copied the defy.tools.relay-worker.js file to our public root directory.
const keyStore = await KeyStoreStorage.fromStorage(KeyStoreFileStorage);
const relayPool = new RelayPool(); // or any class that extends RelayPool
relayPool.addRelay('wss://my.relay.url', true, true, true);
setDefyContext({ keyStore, relayPool });
keyStore.load().subscribe(result => {
const { channels, contacts } = result;
// do stuff
})
To use RelayPool
as a Web Worker, make sure the defy.tools.relay-worker.js script is copied to your web project public root path. Here is a simple custom vite plugin example that can do that automatically. Then you can add the copied file to your .gitignore .
plugins: [
...
{
name: 'copy-relay-worker',
buildStart: async () => {
const relayWorkerJs = 'defy.tools.relay-worker.js';
const srcJs = `${__dirname}/node_modules/defy.tools/dist/${relayWorkerJs}`;
const srcMap = `${__dirname}/node_modules/defy.tools/dist/${relayWorkerJs}.map`;
fs.copyFileSync(srcJs, `${__dirname}/public/${relayWorkerJs}`);
if (fs.existsSync(srcMap)) {
fs.copyFileSync(srcMap, `${__dirname}/public/${relayWorkerJs}.map`);
}
}
},
],
Due to the nature of sequential indexes, we should not create new documents on the root index until we know it is loaded with the existing documents. We will create a documents meta object to help make this faster later. But still we would need to wait for that too.
Example assumes RelayPool in context and the keyStore
load()
has been called and the documentsIndex is in the 'ready' state.
const { keyStore } = useDefyContext();
const info: PrivateChannelPayload = {
kind: NostrKinds.ChannelMetadata,
pubkey: keyStore.ownerPubKey,
name: 'New Channel',
created_at: getNowSeconds(),
contacts: {},
shares:{}
};
// assume we set the relayPool in the context earlier
keyStore.load().pipe(
mergeMap(() => PrivateChannel.create(info, true)),
).subscribe(channel => {
// do stuff with the channel
});
Example assumes RelayPool in context
const { keyStore } = useDefyContext();
// determine your the channel:
const channel = keyStore.channels[0];
channel.saveNote('Hello World').subscribe()
Example assumes RelayPool in context
const note = channel.posts[0]; // find the note to reply / react to
// a reply
note.reply('Hi', 1).subscribe();
// a reaction
note.reply('👍', 7).subscribe();
Example assumes RelayPool in context
const { keyStore } = useDefyContext();
const channel = keyStore.channels[0];
const [sub$, endSub] = channel.listen();
sub$.subscribe(note => {
// do stuff with the note
// consider also using other rxjs operators to buffer
// and/or otherwise control the flow of messages
});
// when finished, clean up with the endSub() to end both
// the rxjs subscription and the close the relay request
endSub();
Example assumes RelayPool in context
const { keyStore } = useDefyContext();
const channel = keyStore.channels[0];
const [sub$, endSub] = channel.dkxPost.query<Note>({
keepAlive: false,
limit: 50
});
sub$.pipe(
reduce((notes, note) => {
notes.push(note);
return notes;
}, [] as Note[]),
).subscribe(notes => {
// do stuff with notes
});
Example assumes RelayPool in context and the keyStore
load()
has been called and the documentsIndex is in the 'ready' state. v1.3.2 supports group conversations with an array of pubkeys.
const { keyStore } = useDefyContext();
const davesPubKey = '828adc3061e4f0c3f1984dce96003eb89d2ab279e703e01de806c9b2ba33ff73';
ContactChannel.create([davesPubKey]).pipe(
mergeMap(msgChannel => {
// until Dave responds, messages are sent to
// him via his Notifications index
return msgChannel.sendMessage("Hi Dave!");
})
).subscribe();
Example assumes RelayPool in context and the keyStore
load()
has been called and the documentsIndex is in the 'ready' state.
const { keyStore } = useDefyContext();
const [sub$, endSub] = keyStore.notifyIndex.listen<Notification>();
sub$.pipe(
// make sure there is a channelId in the notification
filter(notif => Boolean(notif.content.tags?.[0]?.[0] === 'e')),
mergeMap(notif => {
const contact = keyStore.contacts.find(x => x.channelId === notif.content.tags![0][1]);
if(contact){
return of(contact);
} else {
return ContactChannel.create(notif, true)
}
})
).subscribe(contact => {
// a saved contact that we can send and receive messages on
console.log(contact);
});
Example assumes RelayPool in context and the keyStore
load()
has been called and the documentsIndex is in the 'ready' state.
const { keyStore } = useDefyContext();
// look for a one on one conversation
const davesPubKey = '828adc3061e4f0c3f1984dce96003eb89d2ab279e703e01de806c9b2ba33ff73';
let contact = keyStore.contacts.find(x => x.content.contacts[davesPubKey]
&& Object.keys(x.indexes).length == 1);
// or look by channelId// or look by channelId
const channelId = ContactChannel.getChannelId([
davesPubKey, keyStore.ownerPubKey
])
contact = keyStore.contacts.find(x => x.channelId === channelId)
const [sub$, endSub] = contact.listen();
sub$.pipe(
bufferTime(1000),
tap(messages => {
console.log(messages);
})
).subscribe();
MIT (c) 2023, 2024 David Krause https://defy.social, see LICENSE file.