From 3266b4d4c2ace4a42be225cfd5987fdc30d7fd81 Mon Sep 17 00:00:00 2001 From: Anderson Juhasc Date: Sat, 4 Jan 2025 13:52:57 -0300 Subject: [PATCH] added NIP-55 --- nip55.test.ts | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++ nip55.ts | 127 +++++++++++++++++++++++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 nip55.test.ts create mode 100644 nip55.ts diff --git a/nip55.test.ts b/nip55.test.ts new file mode 100644 index 0000000..d2bdb1b --- /dev/null +++ b/nip55.test.ts @@ -0,0 +1,183 @@ +import { test, expect } from 'bun:test' +import * as nip55 from './nip55.js' + +// Function to parse the NostrSigner URI +function parseNostrSignerUri(uri) { + const [base, query] = uri.split('?') + const basePart = base.replace('nostrsigner:', '') + + let jsonObject = null + if (basePart) { + try { + jsonObject = JSON.parse(decodeURIComponent(basePart)) + } catch (e) { + console.warn('Failed to parse base JSON:', e) + } + } + + const urlSearchParams = new URLSearchParams(query) + const queryParams = Object.fromEntries(urlSearchParams.entries()) + if (queryParams.permissions) { + queryParams.permissions = JSON.parse(decodeURIComponent(queryParams.permissions)) + } + + return { + base: jsonObject, + ...queryParams, + } +} + +// Test cases +test('Get Public Key URI', () => { + const permissions = [{ type: 'sign_event', kind: 22242 }, { type: 'nip44_decrypt' }] + const callbackUrl = 'https://example.com/?event=' + + const uri = nip55.getPublicKeyUri({ + permissions, + callbackUrl, + }) + + const jsonObject = parseNostrSignerUri(uri) + + expect(jsonObject).toHaveProperty('type', 'get_public_key') + expect(jsonObject).toHaveProperty('compressionType', 'none') + expect(jsonObject).toHaveProperty('returnType', 'signature') + expect(jsonObject).toHaveProperty('callbackUrl', 'https://example.com/?event=') + expect(jsonObject).toHaveProperty('permissions[0].type', 'sign_event') + expect(jsonObject).toHaveProperty('permissions[0].kind', 22242) + expect(jsonObject).toHaveProperty('permissions[1].type', 'nip44_decrypt') +}) + +test('Sign Event URI', () => { + const eventJson = { kind: 1, content: 'test' } + + const uri = nip55.signEventUri({ + eventJson, + id: 'some_id', + currentUser: 'hex_pub_key', + }) + + const jsonObject = parseNostrSignerUri(uri) + + expect(jsonObject).toHaveProperty('base.kind', 1) + expect(jsonObject).toHaveProperty('base.content', 'test') + expect(jsonObject).toHaveProperty('type', 'sign_event') + expect(jsonObject).toHaveProperty('compressionType', 'none') + expect(jsonObject).toHaveProperty('returnType', 'signature') + expect(jsonObject).toHaveProperty('id', 'some_id') + expect(jsonObject).toHaveProperty('current_user', 'hex_pub_key') +}) + +test('Get Relays URI', () => { + const uri = nip55.getRelaysUri({ + id: 'some_id', + currentUser: 'hex_pub_key', + appName: 'test app name', + }) + + const jsonObject = parseNostrSignerUri(uri) + + expect(jsonObject).toHaveProperty('type', 'get_relays') + expect(jsonObject).toHaveProperty('compressionType', 'none') + expect(jsonObject).toHaveProperty('returnType', 'signature') + expect(jsonObject).toHaveProperty('id', 'some_id') + expect(jsonObject).toHaveProperty('current_user', 'hex_pub_key') + expect(jsonObject).toHaveProperty('appName', 'test app name') +}) + +test('Encrypt NIP-04 URI', () => { + const callbackUrl = 'https://example.com/?event=' + + const uri = nip55.encryptNip04Uri({ + callbackUrl, + pubKey: 'hex_pub_key', + content: 'plainText', + }) + + const jsonObject = parseNostrSignerUri(uri) + + expect(jsonObject).toHaveProperty('type', 'nip04_encrypt') + expect(jsonObject).toHaveProperty('compressionType', 'none') + expect(jsonObject).toHaveProperty('returnType', 'signature') + expect(jsonObject).toHaveProperty('callbackUrl', callbackUrl) + expect(jsonObject).toHaveProperty('pubKey', 'hex_pub_key') + expect(jsonObject).toHaveProperty('plainText', 'plainText') +}) + +test('Decrypt NIP-04 URI', () => { + const uri = nip55.decryptNip04Uri({ + id: 'some_id', + currentUser: 'hex_pub_key', + pubKey: 'hex_pub_key', + content: 'encryptedText', + }) + + const jsonObject = parseNostrSignerUri(uri) + + expect(jsonObject).toHaveProperty('type', 'nip04_decrypt') + expect(jsonObject).toHaveProperty('compressionType', 'none') + expect(jsonObject).toHaveProperty('returnType', 'signature') + expect(jsonObject).toHaveProperty('id', 'some_id') + expect(jsonObject).toHaveProperty('current_user', 'hex_pub_key') + expect(jsonObject).toHaveProperty('pubKey', 'hex_pub_key') + expect(jsonObject).toHaveProperty('encryptedText', 'encryptedText') +}) + +test('Encrypt NIP-44 URI', () => { + const uri = nip55.encryptNip44Uri({ + id: 'some_id', + currentUser: 'hex_pub_key', + pubKey: 'hex_pub_key', + content: 'plainText', + }) + + const jsonObject = parseNostrSignerUri(uri) + + expect(jsonObject).toHaveProperty('type', 'nip44_encrypt') + expect(jsonObject).toHaveProperty('compressionType', 'none') + expect(jsonObject).toHaveProperty('returnType', 'signature') + expect(jsonObject).toHaveProperty('id', 'some_id') + expect(jsonObject).toHaveProperty('current_user', 'hex_pub_key') + expect(jsonObject).toHaveProperty('pubKey', 'hex_pub_key') + expect(jsonObject).toHaveProperty('plainText', 'plainText') +}) + +test('Decrypt NIP-44 URI', () => { + const uri = nip55.decryptNip44Uri({ + id: 'some_id', + currentUser: 'hex_pub_key', + pubKey: 'hex_pub_key', + content: 'encryptedText', + }) + + const jsonObject = parseNostrSignerUri(uri) + + expect(jsonObject).toHaveProperty('type', 'nip44_decrypt') + expect(jsonObject).toHaveProperty('compressionType', 'none') + expect(jsonObject).toHaveProperty('returnType', 'signature') + expect(jsonObject).toHaveProperty('id', 'some_id') + expect(jsonObject).toHaveProperty('current_user', 'hex_pub_key') + expect(jsonObject).toHaveProperty('pubKey', 'hex_pub_key') + expect(jsonObject).toHaveProperty('encryptedText', 'encryptedText') +}) + +test('Decrypt Zap Event URI', () => { + const eventJson = { kind: 1, content: 'test' } + + const uri = nip55.decryptZapEventUri({ + eventJson, + id: 'some_id', + currentUser: 'hex_pub_key', + returnType: 'event', + compressionType: 'gzip', + }) + + const jsonObject = parseNostrSignerUri(uri) + + expect(jsonObject).toHaveProperty('type', 'decrypt_zap_event') + expect(jsonObject).toHaveProperty('compressionType', 'gzip') + expect(jsonObject).toHaveProperty('returnType', 'event') + expect(jsonObject).toHaveProperty('base.kind', 1) + expect(jsonObject).toHaveProperty('id', 'some_id') + expect(jsonObject).toHaveProperty('current_user', 'hex_pub_key') +}) diff --git a/nip55.ts b/nip55.ts new file mode 100644 index 0000000..10c57cb --- /dev/null +++ b/nip55.ts @@ -0,0 +1,127 @@ +type BaseParams = { + callbackUrl?: string + returnType?: 'signature' | 'event' + compressionType?: 'none' | 'gzip' +} + +type PermissionsParams = BaseParams & { + permissions?: { type: string; kind?: number }[] +} + +type EventUriParams = BaseParams & { + eventJson: Record + id?: string + currentUser?: string +} + +type EncryptDecryptParams = BaseParams & { + pubKey: string + content: string + id?: string + currentUser?: string +} + +type UriParams = BaseParams & { + base: string + type: string + id?: string + currentUser?: string + permissions?: { type: string; kind?: number }[] + pubKey?: string + plainText?: string + encryptedText?: string + appName?: string +} + +function encodeParams(params: Record): string { + return new URLSearchParams(params as Record).toString() +} + +function filterUndefined>(obj: T): T { + return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined)) as T +} + +function buildUri({ + base, + type, + callbackUrl, + returnType = 'signature', + compressionType = 'none', + ...params +}: UriParams): string { + const baseParams = { + type, + compressionType, + returnType, + callbackUrl, + id: params.id, + current_user: params.currentUser, + permissions: + params.permissions && params.permissions.length > 0 + ? encodeURIComponent(JSON.stringify(params.permissions)) + : undefined, + pubKey: params.pubKey, + plainText: params.plainText, + encryptedText: params.encryptedText, + appName: params.appName, + } + + const filteredParams = filterUndefined(baseParams) + return `${base}?${encodeParams(filteredParams)}` +} + +function buildDefaultUri(type: string, params: Partial): string { + return buildUri({ + base: 'nostrsigner:', + type, + ...params, + }) +} + +export function getPublicKeyUri({ permissions = [], ...params }: PermissionsParams): string { + return buildDefaultUri('get_public_key', { permissions, ...params }) +} + +export function signEventUri({ eventJson, ...params }: EventUriParams): string { + return buildUri({ + base: `nostrsigner:${encodeURIComponent(JSON.stringify(eventJson))}`, + type: 'sign_event', + ...params, + }) +} + +export function getRelaysUri(params: BaseParams & { id?: string; currentUser?: string }): string { + return buildDefaultUri('get_relays', params) +} + +function encryptUri(type: 'nip44_encrypt' | 'nip04_encrypt', params: EncryptDecryptParams): string { + return buildDefaultUri(type, { ...params, plainText: params.content }) +} + +function decryptUri(type: 'nip44_decrypt' | 'nip04_decrypt', params: EncryptDecryptParams): string { + return buildDefaultUri(type, { ...params, encryptedText: params.content }) +} + +export function encryptNip04Uri(params: EncryptDecryptParams): string { + return encryptUri('nip04_encrypt', params) +} + +export function decryptNip04Uri(params: EncryptDecryptParams): string { + return decryptUri('nip04_decrypt', params) +} + +export function encryptNip44Uri(params: EncryptDecryptParams): string { + return encryptUri('nip44_encrypt', params) +} + +export function decryptNip44Uri(params: EncryptDecryptParams): string { + return decryptUri('nip44_decrypt', params) +} + +export function decryptZapEventUri({ eventJson, ...params }: EventUriParams): string { + return buildUri({ + base: `nostrsigner:${encodeURIComponent(JSON.stringify(eventJson))}`, + type: 'decrypt_zap_event', + ...params, + }) +}