mirror of
https://github.com/nbd-wtf/nostr-tools.git
synced 2025-12-08 16:28:49 +00:00
Compare commits
4 Commits
v1.0.0-rc2
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87c0f0d061 | ||
|
|
83c397b839 | ||
|
|
cd7d1cec48 | ||
|
|
613a843838 |
41
README.md
41
README.md
@@ -35,10 +35,10 @@ let event = {
|
|||||||
|
|
||||||
event.id = getEventHash(event)
|
event.id = getEventHash(event)
|
||||||
event.pubkey = getPublicKey(privateKey)
|
event.pubkey = getPublicKey(privateKey)
|
||||||
event.sig = await signEvent(event, privateKey)
|
event.sig = signEvent(event, privateKey)
|
||||||
|
|
||||||
let ok = validateEvent(event)
|
let ok = validateEvent(event)
|
||||||
let veryOk = await verifySignature(event)
|
let veryOk = verifySignature(event)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Interacting with a relay
|
### Interacting with a relay
|
||||||
@@ -98,7 +98,7 @@ let event = {
|
|||||||
content: 'hello world'
|
content: 'hello world'
|
||||||
}
|
}
|
||||||
event.id = getEventHash(event)
|
event.id = getEventHash(event)
|
||||||
event.sig = await signEvent(event, sk)
|
event.sig = signEvent(event, sk)
|
||||||
|
|
||||||
let pub = relay.publish(event)
|
let pub = relay.publish(event)
|
||||||
pub.on('ok', () => {
|
pub.on('ok', () => {
|
||||||
@@ -202,6 +202,41 @@ sub.on('event', (event) => {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Performing and checking for delegation
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {nip26, getPublicKey, generatePrivateKey} from 'nostr-tools'
|
||||||
|
|
||||||
|
// delegator
|
||||||
|
let sk1 = generatePrivateKey()
|
||||||
|
let pk1 = getPublicKey(sk1)
|
||||||
|
|
||||||
|
// delegatee
|
||||||
|
let sk2 = generatePrivateKey()
|
||||||
|
let pk2 = getPublicKey(sk2)
|
||||||
|
|
||||||
|
// generate delegation
|
||||||
|
let delegation = nip26.createDelegation(sk1, {
|
||||||
|
pubkey: pk2,
|
||||||
|
kind: 1,
|
||||||
|
since: Math.round(Date.now() / 1000),
|
||||||
|
until: Math.round(Date.now() / 1000) + 60 * 60 * 24 * 30 /* 30 days */
|
||||||
|
})
|
||||||
|
|
||||||
|
// the delegatee uses the delegation when building an event
|
||||||
|
let event = {
|
||||||
|
pubkey: pk2,
|
||||||
|
kind: 1,
|
||||||
|
created_at: Math.round(Date.now() / 1000),
|
||||||
|
content: 'hello from a delegated key',
|
||||||
|
tags: [['delegation', delegation.from, delegation.cond, delegation.sig]]
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally any receiver of this event can check for the presence of a valid delegation tag
|
||||||
|
let delegator = nip26.getDelegator(event)
|
||||||
|
assert(delegator === pk1) // will be null if there is no delegation tag or if it is invalid
|
||||||
|
```
|
||||||
|
|
||||||
Please consult the tests or [the source code](https://github.com/fiatjaf/nostr-tools) for more information that isn't available here.
|
Please consult the tests or [the source code](https://github.com/fiatjaf/nostr-tools) for more information that isn't available here.
|
||||||
|
|
||||||
### Using from the browser (if you don't want to use a bundler)
|
### Using from the browser (if you don't want to use a bundler)
|
||||||
|
|||||||
27
event.ts
27
event.ts
@@ -3,10 +3,27 @@ import {sha256} from '@noble/hashes/sha256'
|
|||||||
|
|
||||||
import {utf8Encoder} from './utils'
|
import {utf8Encoder} from './utils'
|
||||||
|
|
||||||
|
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
export enum Kind {
|
||||||
|
Metadata = 0,
|
||||||
|
Text = 1,
|
||||||
|
RecommendRelay = 2,
|
||||||
|
Contacts = 3,
|
||||||
|
EncryptedDirectMessage = 4,
|
||||||
|
EventDeletion = 5,
|
||||||
|
Reaction = 7,
|
||||||
|
ChannelCreation = 40,
|
||||||
|
ChannelMetadata = 41,
|
||||||
|
ChannelMessage = 42,
|
||||||
|
ChannelHideMessage = 43,
|
||||||
|
ChannelMuteUser = 44,
|
||||||
|
}
|
||||||
|
|
||||||
export type Event = {
|
export type Event = {
|
||||||
id?: string
|
id?: string
|
||||||
sig?: string
|
sig?: string
|
||||||
kind: number
|
kind: Kind
|
||||||
tags: string[][]
|
tags: string[][]
|
||||||
pubkey: string
|
pubkey: string
|
||||||
content: string
|
content: string
|
||||||
@@ -58,12 +75,12 @@ export function validateEvent(event: Event): boolean {
|
|||||||
|
|
||||||
export function verifySignature(
|
export function verifySignature(
|
||||||
event: Event & {id: string; sig: string}
|
event: Event & {id: string; sig: string}
|
||||||
): Promise<boolean> {
|
): boolean {
|
||||||
return secp256k1.schnorr.verify(event.sig, event.id, event.pubkey)
|
return secp256k1.schnorr.verifySync(event.sig, event.id, event.pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function signEvent(event: Event, key: string): Promise<string> {
|
export function signEvent(event: Event, key: string): string {
|
||||||
return secp256k1.utils.bytesToHex(
|
return secp256k1.utils.bytesToHex(
|
||||||
await secp256k1.schnorr.sign(getEventHash(event), key)
|
secp256k1.schnorr.signSync(getEventHash(event), key)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
10
index.ts
10
index.ts
@@ -7,3 +7,13 @@ export * as nip04 from './nip04'
|
|||||||
export * as nip05 from './nip05'
|
export * as nip05 from './nip05'
|
||||||
export * as nip06 from './nip06'
|
export * as nip06 from './nip06'
|
||||||
export * as nip19 from './nip19'
|
export * as nip19 from './nip19'
|
||||||
|
export * as nip26 from './nip26'
|
||||||
|
|
||||||
|
// monkey patch secp256k1
|
||||||
|
import * as secp256k1 from '@noble/secp256k1'
|
||||||
|
import {hmac} from '@noble/hashes/hmac'
|
||||||
|
import {sha256} from '@noble/hashes/sha256'
|
||||||
|
secp256k1.utils.hmacSha256Sync = (key, ...msgs) =>
|
||||||
|
hmac(sha256, key, secp256k1.utils.concatBytes(...msgs))
|
||||||
|
secp256k1.utils.sha256Sync = (...msgs) =>
|
||||||
|
sha256(secp256k1.utils.concatBytes(...msgs))
|
||||||
|
|||||||
105
nip26.test.js
Normal file
105
nip26.test.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/* eslint-env jest */
|
||||||
|
|
||||||
|
const {nip26, getPublicKey, generatePrivateKey} = require('./lib/nostr.cjs')
|
||||||
|
|
||||||
|
test('parse good delegation from NIP', async () => {
|
||||||
|
expect(
|
||||||
|
nip26.getDelegator({
|
||||||
|
id: 'a080fd288b60ac2225ff2e2d815291bd730911e583e177302cc949a15dc2b2dc',
|
||||||
|
pubkey:
|
||||||
|
'62903b1ff41559daf9ee98ef1ae67cc52f301bb5ce26d14baba3052f649c3f49',
|
||||||
|
created_at: 1660896109,
|
||||||
|
kind: 1,
|
||||||
|
tags: [
|
||||||
|
[
|
||||||
|
'delegation',
|
||||||
|
'86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e',
|
||||||
|
'kind=1&created_at>1640995200',
|
||||||
|
'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
content: 'Hello world',
|
||||||
|
sig: 'cd4a3cd20dc61dcbc98324de561a07fd23b3d9702115920c0814b5fb822cc5b7c5bcdaf3fa326d24ed50c5b9c8214d66c75bae34e3a84c25e4d122afccb66eb6'
|
||||||
|
})
|
||||||
|
).toEqual('86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parse bad delegations', async () => {
|
||||||
|
expect(
|
||||||
|
nip26.getDelegator({
|
||||||
|
id: 'a080fd288b60ac2225ff2e2d815291bd730911e583e177302cc949a15dc2b2dc',
|
||||||
|
pubkey:
|
||||||
|
'62903b1ff41559daf9ee98ef1ae67cc52f301bb5ce26d14baba3052f649c3f49',
|
||||||
|
created_at: 1660896109,
|
||||||
|
kind: 1,
|
||||||
|
tags: [
|
||||||
|
[
|
||||||
|
'delegation',
|
||||||
|
'86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42f',
|
||||||
|
'kind=1&created_at>1640995200',
|
||||||
|
'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
content: 'Hello world',
|
||||||
|
sig: 'cd4a3cd20dc61dcbc98324de561a07fd23b3d9702115920c0814b5fb822cc5b7c5bcdaf3fa326d24ed50c5b9c8214d66c75bae34e3a84c25e4d122afccb66eb6'
|
||||||
|
})
|
||||||
|
).toEqual(null)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
nip26.getDelegator({
|
||||||
|
id: 'a080fd288b60ac2225ff2e2d815291bd730911e583e177302cc949a15dc2b2dc',
|
||||||
|
pubkey:
|
||||||
|
'62903b1ff41559daf9ee98ef1ae67cc52f301bb5ce26d14baba3052f649c3f49',
|
||||||
|
created_at: 1660896109,
|
||||||
|
kind: 1,
|
||||||
|
tags: [
|
||||||
|
[
|
||||||
|
'delegation',
|
||||||
|
'86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e',
|
||||||
|
'kind=1&created_at>1740995200',
|
||||||
|
'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
content: 'Hello world',
|
||||||
|
sig: 'cd4a3cd20dc61dcbc98324de561a07fd23b3d9702115920c0814b5fb822cc5b7c5bcdaf3fa326d24ed50c5b9c8214d66c75bae34e3a84c25e4d122afccb66eb6'
|
||||||
|
})
|
||||||
|
).toEqual(null)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
nip26.getDelegator({
|
||||||
|
id: 'a080fd288b60ac2225ff2e2d815291bd730911e583e177302cc949a15dc2b2dc',
|
||||||
|
pubkey:
|
||||||
|
'62903b1ff41559daf9ee98ef1ae67c152f301bb5ce26d14baba3052f649c3f49',
|
||||||
|
created_at: 1660896109,
|
||||||
|
kind: 1,
|
||||||
|
tags: [
|
||||||
|
[
|
||||||
|
'delegation',
|
||||||
|
'86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e',
|
||||||
|
'kind=1&created_at>1640995200',
|
||||||
|
'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
content: 'Hello world',
|
||||||
|
sig: 'cd4a3cd20dc61dcbc98324de561a07fd23b3d9702115920c0814b5fb822cc5b7c5bcdaf3fa326d24ed50c5b9c8214d66c75bae34e3a84c25e4d122afccb66eb6'
|
||||||
|
})
|
||||||
|
).toEqual(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('create and verify delegation', async () => {
|
||||||
|
let sk1 = generatePrivateKey()
|
||||||
|
let pk1 = getPublicKey(sk1)
|
||||||
|
let sk2 = generatePrivateKey()
|
||||||
|
let pk2 = getPublicKey(sk2)
|
||||||
|
let delegation = nip26.createDelegation(sk1, {pubkey: pk2, kind: 1})
|
||||||
|
expect(delegation).toHaveProperty('from', pk1)
|
||||||
|
expect(delegation).toHaveProperty('to', pk2)
|
||||||
|
expect(delegation).toHaveProperty('cond', 'kind=1')
|
||||||
|
|
||||||
|
let event = {
|
||||||
|
kind: 1,
|
||||||
|
tags: [['delegation', delegation.from, delegation.cond, delegation.sig]],
|
||||||
|
pubkey: pk2
|
||||||
|
}
|
||||||
|
expect(nip26.getDelegator(event)).toEqual(pk1)
|
||||||
|
})
|
||||||
90
nip26.ts
Normal file
90
nip26.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import * as secp256k1 from '@noble/secp256k1'
|
||||||
|
import {sha256} from '@noble/hashes/sha256'
|
||||||
|
|
||||||
|
import {Event} from './event'
|
||||||
|
import {utf8Encoder} from './utils'
|
||||||
|
import {getPublicKey} from './keys'
|
||||||
|
|
||||||
|
export type Parameters = {
|
||||||
|
pubkey: string // the key to whom the delegation will be given
|
||||||
|
kind: number | undefined
|
||||||
|
until: number | undefined // delegation will only be valid until this date
|
||||||
|
since: number | undefined // delegation will be valid from this date on
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Delegation = {
|
||||||
|
from: string // the pubkey who signed the delegation
|
||||||
|
to: string // the pubkey that is allowed to use the delegation
|
||||||
|
cond: string // the string of conditions as they should be included in the event tag
|
||||||
|
sig: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDelegation(
|
||||||
|
privateKey: string,
|
||||||
|
parameters: Parameters
|
||||||
|
): Delegation {
|
||||||
|
let conditions = []
|
||||||
|
if ((parameters.kind || -1) >= 0) conditions.push(`kind=${parameters.kind}`)
|
||||||
|
if (parameters.until) conditions.push(`created_at<${parameters.until}`)
|
||||||
|
if (parameters.since) conditions.push(`created_at>${parameters.since}`)
|
||||||
|
let cond = conditions.join('&')
|
||||||
|
|
||||||
|
if (cond === '')
|
||||||
|
throw new Error('refusing to create a delegation without any conditions')
|
||||||
|
|
||||||
|
let sighash = sha256(
|
||||||
|
utf8Encoder.encode(`nostr:delegation:${parameters.pubkey}:${cond}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
let sig = secp256k1.utils.bytesToHex(
|
||||||
|
secp256k1.schnorr.signSync(sighash, privateKey)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: getPublicKey(privateKey),
|
||||||
|
to: parameters.pubkey,
|
||||||
|
cond,
|
||||||
|
sig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDelegator(event: Event): string | null {
|
||||||
|
// find delegation tag
|
||||||
|
let tag = event.tags.find(tag => tag[0] === 'delegation' && tag.length >= 4)
|
||||||
|
if (!tag) return null
|
||||||
|
|
||||||
|
let pubkey = tag[1]
|
||||||
|
let cond = tag[2]
|
||||||
|
let sig = tag[3]
|
||||||
|
|
||||||
|
// check conditions
|
||||||
|
let conditions = cond.split('&')
|
||||||
|
for (let i = 0; i < conditions.length; i++) {
|
||||||
|
let [key, operator, value] = conditions[i].split(/\b/)
|
||||||
|
|
||||||
|
// the supported conditions are just 'kind' and 'created_at' for now
|
||||||
|
if (key === 'kind' && operator === '=' && event.kind === parseInt(value))
|
||||||
|
continue
|
||||||
|
else if (
|
||||||
|
key === 'created_at' &&
|
||||||
|
operator === '<' &&
|
||||||
|
event.created_at < parseInt(value)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
else if (
|
||||||
|
key === 'created_at' &&
|
||||||
|
operator === '>' &&
|
||||||
|
event.created_at > parseInt(value)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
else return null // invalid condition
|
||||||
|
}
|
||||||
|
|
||||||
|
// check signature
|
||||||
|
let sighash = sha256(
|
||||||
|
utf8Encoder.encode(`nostr:delegation:${event.pubkey}:${cond}`)
|
||||||
|
)
|
||||||
|
if (!secp256k1.schnorr.verifySync(sig, sighash, pubkey)) return null
|
||||||
|
|
||||||
|
return pubkey
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nostr-tools",
|
"name": "nostr-tools",
|
||||||
"version": "1.0.0-rc2",
|
"version": "1.0.0",
|
||||||
"description": "Tools for making a Nostr client.",
|
"description": "Tools for making a Nostr client.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
Reference in New Issue
Block a user