web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

commit fe2572f9ecc0810137b50d366a536e6664a12330
parent 1dd4db47e6b3629a88862112b1f50aa6a7348ce6
Author: triesap <137732411+triesap@users.noreply.github.com>
Date:   Tue,  4 Mar 2025 07:47:13 +0000

utils-nostr: migrate utils from `@radroots/util`, add minor edits

Diffstat:
Mutils-nostr/.gitignore | 2++
Mutils-nostr/package.json | 5++++-
Mutils-nostr/src/index.ts | 11+++++++++--
Autils-nostr/src/lib/events.ts | 46++++++++++++++++++++++++++++++++++++++++++++++
Autils-nostr/src/lib/keys.ts | 43+++++++++++++++++++++++++++++++++++++++++++
Autils-nostr/src/lib/ndk.ts | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils-nostr/src/lib/types.ts | 20++++++++++++++++++++
Autils-nostr/src/services/events/lib.ts | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils-nostr/src/services/events/types.ts | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils-nostr/src/services/keys/lib.ts | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autils-nostr/src/services/keys/types.ts | 11+++++++++++
Autils-nostr/src/util.ts | 6++++++
12 files changed, 513 insertions(+), 3 deletions(-)

diff --git a/utils-nostr/.gitignore b/utils-nostr/.gitignore @@ -44,3 +44,4 @@ vite.config.ts.timestamp-* notes*.txt notes*.md git-diff.txt +justfile +\ No newline at end of file diff --git a/utils-nostr/package.json b/utils-nostr/package.json @@ -18,6 +18,7 @@ "watch": "tsc -w" }, "devDependencies": { + "@types/uuid": "^10.0.0", "tsup": "^6.2.3", "typescript": "^5.3.3" }, @@ -28,6 +29,8 @@ "@noble/curves": "^1.6.0", "@noble/hashes": "^1.4.0", "@nostr-dev-kit/ndk": "^2.11.0", - "nostr-tools": "^2.10.4" + "nostr-geotags": "^0.7.1", + "nostr-tools": "^2.10.4", + "uuid": "^10.0.0" } } \ No newline at end of file diff --git a/utils-nostr/src/index.ts b/utils-nostr/src/index.ts @@ -1 +1,9 @@ -export const rad = `roots` -\ No newline at end of file +export * from "./lib/events" +export * from "./lib/keys" +export * from "./lib/ndk" +export * from "./lib/types" +export * from "./services/events/lib" +export * from "./services/events/types" +export * from "./services/keys/lib" +export * from "./services/keys/types" +export * from "./util" diff --git a/utils-nostr/src/lib/events.ts b/utils-nostr/src/lib/events.ts @@ -0,0 +1,45 @@ +import { INostrEventServiceNeventEncode, uuidv4, type INostrEventEventSign } from "$root"; +import { schnorr } from "@noble/curves/secp256k1"; +import { hexToBytes } from "@noble/hashes/utils"; +import { finalizeEvent, getEventHash, nip19, type NostrEvent as NostrToolsEvent } from "nostr-tools"; + +export const lib_nostr_event_verify = (event: NostrToolsEvent): boolean => { + const hash = getEventHash(event); + if (hash !== event.id) return false + const valid = schnorr.verify(event.sig, hash, event.pubkey); + return valid; +}; + +export const lib_nostr_event_sign = (opts: INostrEventEventSign): NostrToolsEvent => { + return finalizeEvent(opts.event, hexToBytes(opts.secret_key)) +}; + +export const lib_nostr_event_sign_attest = (secret_key: string): NostrToolsEvent => { + return lib_nostr_event_sign({ + secret_key, + event: { + kind: 1, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: uuidv4(), + }, + }); +}; + + +export const lib_nostr_event_verify_serialized = async (event_serialized: string): Promise<{ public_key: string } | undefined> => { + try { + const event = JSON.parse(event_serialized); + const hash = getEventHash(event); + if (hash !== event.id) return undefined; + const valid = schnorr.verify(event.sig, hash, event.pubkey); + if (valid) return { public_key: String(event.pubkey) }; + return undefined; + } catch { + return undefined; + } +}; + +export const lib_nostr_nevent_encode = (opts: INostrEventServiceNeventEncode): string => { + return nip19.neventEncode(opts); +}; +\ No newline at end of file diff --git a/utils-nostr/src/lib/keys.ts b/utils-nostr/src/lib/keys.ts @@ -0,0 +1,42 @@ +import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; +import { generateSecretKey, getPublicKey, nip19 } from "nostr-tools"; + +export const lib_nostr_get_key_bytes = (hex: string): Uint8Array => { + return hexToBytes(hex); +}; + +export const lib_nostr_get_key_hex = (bytes: Uint8Array): string => { + return bytesToHex(bytes); +}; + +export const lib_nostr_key_generate = (): string => { + const bytes = generateSecretKey(); + return lib_nostr_get_key_hex(bytes); +}; + +export const lib_nostr_nsec_encode = (secret_key_hex?: string): string | undefined => { + if (!secret_key_hex) return undefined; + const bytes = lib_nostr_get_key_bytes(secret_key_hex); + return nip19.nsecEncode(bytes); +}; + +export const lib_nostr_nsec_decode = (nsec?: string): string | undefined => { + if (!nsec) return undefined; + const decode = nip19.decode(nsec); + if (decode && decode.type === `nsec` && decode.data) return bytesToHex(decode.data); + return undefined; +}; + + +export const lib_nostr_public_key = (secret_key_hex: string): string => { + const bytes = lib_nostr_get_key_bytes(secret_key_hex); + return getPublicKey(bytes); +}; + +export const lib_nostr_secret_key_validate = (secret_key: string): string | undefined => { + const is_valid_hex = lib_nostr_public_key(secret_key); + if (is_valid_hex) return secret_key; + const is_valid_nsec = lib_nostr_nsec_decode(secret_key); + if (is_valid_nsec) return is_valid_nsec; + return undefined; +}; +\ No newline at end of file diff --git a/utils-nostr/src/lib/ndk.ts b/utils-nostr/src/lib/ndk.ts @@ -0,0 +1,74 @@ +import { time_now_ms, type NostrMetadataTmp } from '$root'; +import NDK, { NDKEvent, NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk'; + +export const ndk_init = async (opts: { + $ndk: NDK; + secret_key: string; +}): Promise<NDKUser | undefined> => { + try { + const { $ndk: ndk, secret_key } = opts; + const signer = new NDKPrivateKeySigner(secret_key); + ndk.signer = signer; + + const user = await signer.user(); + if (user) { + user.ndk = ndk; + return user; + } + } catch (e) { + console.log(`(error) ndk_init `, e); + }; +}; + +export const ndk_event_metadata = async (opts: { + $ndk: NDK; + $ndk_user: NDKUser; + metadata: NostrMetadataTmp +}): Promise<NDKEvent | undefined> => { + try { + const { $ndk, $ndk_user } = opts; + const ev = await ndk_event({ + $ndk, + $ndk_user, + basis: { + kind: 0, + content: JSON.stringify(opts.metadata), + }, + }); + return ev; + } catch (e) { + console.log(`(error) ndk_event_metadata `, e); + } +}; + +export const ndk_event = async (opts: { + $ndk: NDK; + $ndk_user: NDKUser; + basis: { + kind: number; + content: string; + tags?: string[][]; + } +}): Promise<NDKEvent | undefined> => { + try { + const { $ndk: ndk, $ndk_user: ndk_user, basis } = opts; + const time_now = time_now_ms(); + + const tags: string[][] = [ + ['published_at', time_now.toString()], + ]; + + if (basis.tags && basis.tags?.length > 0) for (const tag of basis.tags) tags.push(tag); + + const event: NDKEvent = new NDKEvent(ndk, { + kind: basis.kind, + pubkey: ndk_user.pubkey, + content: basis.content, + created_at: time_now, + tags + }); + return event; + } catch (e) { + console.log(`(error) ndk_event `, e); + }; +}; diff --git a/utils-nostr/src/lib/types.ts b/utils-nostr/src/lib/types.ts @@ -0,0 +1,19 @@ +import { type EventTemplate as NostrToolsEventTemplate } from "nostr-tools"; + +export type NostrMetadataTmp = { + name?: string; + display_name?: string; + about?: string; + website?: string; + picture?: string; + banner?: string; + nip05?: string; + lud06?: string; + lud16?: string; + bot?: boolean; +}; + +export type INostrEventEventSign = { + secret_key: string; + event: NostrToolsEventTemplate; +} +\ No newline at end of file diff --git a/utils-nostr/src/services/events/lib.ts b/utils-nostr/src/services/events/lib.ts @@ -0,0 +1,96 @@ +import { INostrEventEventSign, INostrEventService, INostrEventServiceFormatTagsBasisNip99, INostrEventServiceNeventEncode, lib_nostr_event_sign, lib_nostr_event_sign_attest, lib_nostr_event_verify, lib_nostr_event_verify_serialized, lib_nostr_nevent_encode, NostrEventTagClient, NostrEventTagLocation, NostrEventTagMediaUpload, NostrEventTagPrice, NostrEventTagQuantity } from "$root"; +import { type NDKEvent } from "@nostr-dev-kit/ndk"; +import ngeotags, { type GeoTags as NostrGeotagsGeotags, type InputData as NostrGeotagsInputData } from "nostr-geotags"; +import { type NostrEvent as NostrToolsEvent } from "nostr-tools"; + +export class NostrEventService implements INostrEventService { + public first_tag_value = (event: NDKEvent, tag_name: string): string => { + const tag = event.getMatchingTags(tag_name)[0]; + return tag ? tag[1] : ""; + } + + private fmt_tag_price = (opts: NostrEventTagPrice): string[] => { + const tag = [`price`, opts.amt, opts.currency, opts.qty_amt, opts.qty_unit]; + return tag; + }; + + + private fmt_tag_quantity = (opts: NostrEventTagQuantity): string[] => { + const tag = [`quantity`, opts.amt, opts.unit]; + if (opts.label) tag.push(opts.label); + return tag; + }; + + private fmt_tag_location = (opts: NostrEventTagLocation): string[] => { + const tag = [`location`]; + if (opts.city) tag.push(opts.city); + if (opts.region_code && !isNaN(parseInt(opts.region_code))) tag.push(opts.region_code); + else if (opts.region) tag.push(opts.region); //@todo + if (opts.country_code) tag.push(opts.country_code); + return tag; + }; + + private fmt_tag_image = (opts: NostrEventTagMediaUpload): string[] => { + const tag = [`image`, opts.url]; + if (opts.size) tag.push(`${opts.size.w}x${opts.size.h}`) + return tag; + }; + + private fmt_tag_client = (opts: NostrEventTagClient, d_tag?: string): string[] => { + const tag = [`client`, opts.name]; + if (d_tag) tag.push(`31990:${opts.pubkey}:${d_tag}`); + tag.push(opts.relay); + return tag; + }; + + private fmt_tag_geotags = (opts: NostrEventTagLocation): NostrGeotagsGeotags[] => { + const data: NostrGeotagsInputData = { + lat: opts.lat, + lon: opts.lng, + city: opts.city, + regionName: opts.region, + countryName: opts.country, + countryCode: opts.country_code + }; + return ngeotags(data, { + geohash: true, + gps: true, + city: true, + iso31662: true, + }); + }; + + public fmt_tags_basis_nip99 = (opts: INostrEventServiceFormatTagsBasisNip99): string[][] => { + const { d_tag, listing, quantity, price, location } = opts; + const tags: string[][] = [[`d`, d_tag]]; + if (opts.client) tags.push(this.fmt_tag_client(opts.client, opts.d_tag)); + for (const [k, v] of Object.entries(listing)) if (v) tags.push([k, v]); + tags.push(this.fmt_tag_quantity(quantity)); + tags.push(this.fmt_tag_price(price)); + tags.push(this.fmt_tag_location(location)); + if (opts.images) for (const image of opts.images) tags.push(this.fmt_tag_image(image)); + tags.push(...this.fmt_tag_geotags(location)); + return tags; + }; + + public nostr_event_sign = (opts: INostrEventEventSign): NostrToolsEvent => { + return lib_nostr_event_sign(opts); + }; + + public nostr_event_sign_attest = (secret_key: string): NostrToolsEvent => { + return lib_nostr_event_sign_attest(secret_key); + }; + + public nostr_event_verify = (event: NostrToolsEvent): boolean => { + return lib_nostr_event_verify(event); + }; + + public nostr_event_verify_serialized = (event_serialized: string): boolean => { + const result = lib_nostr_event_verify_serialized(event_serialized); + return !!result; + }; + + public nevent_encode = (opts: INostrEventServiceNeventEncode): string => { + return lib_nostr_nevent_encode(opts); + }; +} +\ No newline at end of file diff --git a/utils-nostr/src/services/events/types.ts b/utils-nostr/src/services/events/types.ts @@ -0,0 +1,80 @@ +import type { INostrEventEventSign } from "$root"; +import { NDKEvent } from "@nostr-dev-kit/ndk"; +import { type NostrEvent as NostrToolsEvent } from "nostr-tools"; + +export type INostrEventServiceFormatTagsBasisNip99 = { + d_tag: string; + listing: NostrEventTagListing; + quantity: NostrEventTagQuantity; + price: NostrEventTagPrice; + location: NostrEventTagLocation; + images?: NostrEventTagMediaUpload[]; + client?: NostrEventTagClient; +}; + +export type INostrEventServiceNeventEncode = { + id: string; + relays: string[]; + author: string; + kind: number; +}; + +export type INostrEventService = { + first_tag_value(event: NDKEvent, tag_name: string): string; + fmt_tags_basis_nip99: (opts: INostrEventServiceFormatTagsBasisNip99) => string[][]; + nostr_event_sign: (opts: INostrEventEventSign) => NostrToolsEvent; + nostr_event_sign_attest: (secret_key: string) => NostrToolsEvent; + nostr_event_verify_serialized: (event_serialized: string) => boolean; + nostr_event_verify: (event: NostrToolsEvent) => boolean; + nevent_encode: (opts: INostrEventServiceNeventEncode) => string; +}; + +export type NostrEventTagListing = { + key: string; + title: string; + category: string; + summary?: string; + process?: string; + lot?: string; + location?: string; + profile?: string; + year?: string; +}; + +export type NostrEventTagPrice = { + amt: string; + currency: string; + qty_amt: string; + qty_unit: string; +}; + +export type NostrEventTagQuantity = { + amt: string; + unit: string; + label?: string; +}; + +export type NostrEventTagLocation = { + city?: string; + region?: string; + region_code?: string; + country?: string; + country_code?: string; + lat: number; + lng: number; + geohash: string; +}; + +export type NostrEventTagMediaUpload = { + url: string; + size?: { + w: number; + h: number; + }; +}; + +export type NostrEventTagClient = { + name: string; + pubkey: string; + relay: string; +}; diff --git a/utils-nostr/src/services/keys/lib.ts b/utils-nostr/src/services/keys/lib.ts @@ -0,0 +1,121 @@ +import { type INostrKeyService, lib_nostr_get_key_bytes, lib_nostr_key_generate, lib_nostr_nsec_decode, lib_nostr_nsec_encode } from '$root'; +import { getPublicKey, nip19 } from 'nostr-tools'; + +export class NostrKeyService implements INostrKeyService { + /** + * + * @returns nostr secret key hex + */ + public generate_key(): string { + return lib_nostr_key_generate(); + }; + + + /** + * + * @returns nostr public key hex from secret key + */ + public public_key(secret_key_hex: string | undefined): string { + try { + if (!secret_key_hex) return ``; + const bytes = lib_nostr_get_key_bytes(secret_key_hex); + const hex = getPublicKey(bytes) + return hex; + } catch (e) { + return `` + } + } + + /** + * + * @returns nostr secret key to public key hex + */ + public publickey_decode(secret_key_hex?: string): string | undefined { + try { + if (secret_key_hex) { + return getPublicKey(lib_nostr_get_key_bytes(secret_key_hex)) + } + return undefined; + } catch (e) { + return undefined; + } + } + + /** + * + * @returns nostr public key npub + */ + public npub(public_key_hex: string | undefined, fallback_to_hex?: boolean): string { + if (!public_key_hex) return ``; + const npub = nip19.npubEncode(public_key_hex); + return npub ? npub : fallback_to_hex ? public_key_hex : ``; + } + + /** + * + * @returns public key hex from npub + */ + public npub_decode(npub: string): string { + const decode = nip19.decode(npub); + console.log(`decode `, decode) + if (decode && decode.type === `npub` && decode.data) return decode.data + return ``; + } + + /** + * + * @returns nostr secret key nsec + */ + public nsec(secret_key_hex?: string): string | undefined { + return lib_nostr_nsec_encode(secret_key_hex); + } + + /** + * + * @returns nostr secret key hex from nsec + */ + public nsec_decode(nsec: string): string | undefined { + return lib_nostr_nsec_decode(nsec); + } + + /** + * + * @returns + */ + public nevent(event_pointer: nip19.EventPointer, relays: string[]): string { + return nip19.neventEncode(event_pointer) + } + + /** + * + * @returns nostr public key nprofile + */ + public nprofile(public_key_hex: string, relays: string[]): string { + if (!public_key_hex || !relays.length) return ``; + return nip19.nprofileEncode({ pubkey: public_key_hex, relays }) + } + + /** + * + * @returns nostr public key nprofile + */ + public nprofile_decode(nprofile: string): [string, string[]] | undefined { + if (!nprofile) return undefined; + const decode = nip19.decode(nprofile); + if (decode && decode.type === `nprofile` && decode.data && decode.data.pubkey && decode.data.relays) return [decode.data.pubkey, decode.data.relays] + return undefined; + } + + /** + * + * @returns + */ + public secretkey_to_publickey(nsec_or_hex: string): string | undefined { + if (nsec_or_hex.startsWith(`nsec1`)) { + return this.nsec_decode(nsec_or_hex); + } else if (nsec_or_hex.length === 64) { + return this.publickey_decode(nsec_or_hex) + } + return undefined; + } +}; diff --git a/utils-nostr/src/services/keys/types.ts b/utils-nostr/src/services/keys/types.ts @@ -0,0 +1,11 @@ +export type INostrKeyService = { + generate_key(): string; + public_key(secret_key_hex: string | undefined): string; + npub(public_key_hex: string | undefined): string; + npub_decode(npub: string): string; + nsec(secret_key_hex: string | undefined): string | undefined; + nsec_decode(nsec: string): string | undefined; + nprofile(public_key_hex: string, relays: string[]): string; + nprofile_decode(nprofile: string): [string, string[]] | undefined; + secretkey_to_publickey(nsec_or_hex: string): string | undefined; +}; diff --git a/utils-nostr/src/util.ts b/utils-nostr/src/util.ts @@ -0,0 +1,5 @@ +import { v4 } from "uuid"; + +export const time_now_ms = (): number => Math.floor(new Date().getTime() / 1000); + +export const uuidv4 = (): string => v4(); +\ No newline at end of file