From 9cd4f16e45cbd35783ee201dd7933bb62f163981 Mon Sep 17 00:00:00 2001 From: Giacomo Gagliano Date: Mon, 10 Jul 2023 17:24:19 +0200 Subject: [PATCH] nip11 - Types, requestRelayInfos() and tests --- nip11.test.ts | 27 +++++ nip11.ts | 313 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 340 insertions(+) create mode 100644 nip11.test.ts create mode 100644 nip11.ts diff --git a/nip11.test.ts b/nip11.test.ts new file mode 100644 index 0000000..ec89689 --- /dev/null +++ b/nip11.test.ts @@ -0,0 +1,27 @@ +import {Nip11} from './nip11' +const requestRelayInfos = Nip11.requestRelayInfos + +describe('requesting Relay infos as for NIP11', () => { + test('testing damus relay', async () => { + const expected_relay_name = 'relay.nostr.nu' + const expected_relay_description = + 'A nostr relay build by Edward Hollander.' + const expected_supported_nips = [ + 1, 2, 4, 9, 11, 12, 15, 16, 20, 22, 26, 28, 33, 40 + ] + + const test_relay = 'https://relay.nostr.nu' + const relay_infos = await requestRelayInfos(test_relay) + const relay_name = relay_infos.name + const relay_description = relay_infos.description + const fees = relay_infos.fees + const admission = fees?.admission + const supported_nips = relay_infos.supported_nips + const admission_condition = Array.isArray(admission) + expect(relay_name).toBe(expected_relay_name) + expect(relay_description).toBe(expected_relay_description) + expect(fees).toBeTruthy() + expect(admission_condition).toBeTruthy() + expect(supported_nips).toMatchObject(expected_supported_nips) + }) +}) diff --git a/nip11.ts b/nip11.ts new file mode 100644 index 0000000..7a8c6d5 --- /dev/null +++ b/nip11.ts @@ -0,0 +1,313 @@ +// #85 I created an implementation of each of the different +// types described in the NIP11. +import fetch from 'node-fetch' + +export namespace Nip11 { + export interface requestRelayInfos< + N extends string[], + A extends boolean, + P extends boolean + > { + (relay_addr: string): Promise> + } + + // I wanted to use an enum, but eslint is giving me + // problems! + + // export enum headers_accept { + // nostr_json = 'application/nostr+json' + // } + + export const requestRelayInfos: requestRelayInfos = ( + relay_addr: string + ) => { + return new Promise(async (res, rej) => { + try { + const accept = 'application/nostr+json' + const init = {headers: {accept}} + const response = await fetch(relay_addr, init) + const relayInfos: RelayInfosTemplate = + (await response.json()) as RelayInfosTemplate + res(relayInfos) + } catch (error) { + rej(error) + } + }) + } +} + +/** + * ## Relay Information Document + + * Relays may provide server metadata to clients to inform + * them of capabilities, administrative contacts, and + * various server attributes. This is made available as a + * JSON document over HTTP, on the same URI as the relay's + * websocket. + + * Any field may be omitted, and clients MUST ignore any + * additional fields they do not understand. Relays MUST + * accept CORS requests by sending + * `Access-Control-Allow-Origin`, + * `Access-Control-Allow-Headers`, and + * `Access-Control-Allow-Methods` headers. + * @param name string identifying relay + * @param description string with detailed information + * @param pubkey administrative contact pubkey + * @param contact: administrative alternate contact + * @param supported_nips a list of NIP numbers supported by + * the relay + * @param software identifying relay software URL + * @param version string version identifier + */ +export interface RelayInfosTemplate { + // string identifying relay + name: string + description: string + pubkey: string + contact: string + supported_nips: N + software: string + version: string + // limitation?: Limitations +} + +/** + * * ## Extra Fields + + * * ### Server Limitations + + * These are limitations imposed by the relay on clients. + * Your client should expect that requests which exceed + * these practical_ limitations are rejected or fail immediately. + * @param max_message_length this is the maximum number of + * bytes for incoming JSON that the relay will attempt to + * decode and act upon. When you send large subscriptions, + * you will be limited by this value. It also effectively + * limits the maximum size of any event. Value is calculated + * from `[` to `]` and is after UTF-8 serialization (so some + * unicode characters will cost 2-3 bytes). It is equal to + * the maximum size of the WebSocket message frame. + * @param max_subscription total number of subscriptions + * that may be active on a single websocket connection to + * this relay. It's possible that authenticated clients with + * a (paid) relationship to the relay may have higher limits. + * @param max_filters maximum number of filter values in + * each subscription. Must be one or higher. + * @param max_limit the relay server will clamp each + * filter's `limit` value to this number. + * This means the client won't be able to get more than this + * number of events from a single subscription filter. This + * clamping is typically done silently by the relay, but + * with this number, you can know that there are additional + * results if you narrowed your filter's time range or other + * parameters. + * @param max_subid_length maximum length of subscription id as a + * string. + * @param min_prefix for `authors` and `ids` filters which + * are to match against a hex prefix, you must provide at + * least this many hex digits in the prefix. + * @param max_event_tags in any event, this is the maximum + * number of elements in the `tags` list. + * @param max_content_length maximum number of characters in + * the `content` field of any event. This is a count of + * unicode characters. After serializing into JSON it may be + * larger (in bytes), and is still subject to the + * max_message_length`, if defined. + * @param min_pow_difficulty new events will require at + * least this difficulty of PoW, based on [NIP-13](13.md), + * or they will be rejected by this server. + * @param auth_required this relay requires [NIP-42](42.md) + * authentication to happen before a new connection may + * perform any other action. Even if set to False, + * authentication may be required for specific actions. + * @param payment_required this relay requires payment + * before a new connection may perform any action. + */ +export interface Limitations { + max_message_length: number + max_subscription: number + max_filters: number + max_limit: number + max_subid_length: number + min_prefix: number + max_event_tags: number + max_content_length: number + min_pow_difficulty: number + auth_required: A + payment_required: P +} + +type range = [L, H] +type anyRange = range +type genericKinds = (number | anyRange)[] +interface RetentionDetails { + kinds: K + time?: number | null + count?: number | null +} +type AnyRetentionDetails = RetentionDetails +/** + * ### Event Retention + + * There may be a cost associated with storing data forever, + * so relays may wish to state retention times. The values + * stated here are defaults for unauthenticated users and + * visitors. Paid users would likely have other policies. + + * Retention times are given in seconds, with `null` + * indicating infinity. If zero is provided, this means the + * event will not be stored at all, and preferably an error + * will be provided when those are received. + * ```json +{ +... + "retention": [ + { "kinds": [0, 1, [5, 7], [40, 49]], "time": 3600 }, + { "kinds": [[40000, 49999]], "time": 100 }, + { "kinds": [[30000, 39999]], "count": 1000 }, + { "time": 3600, "count": 10000 } + ] +... +} +``` + * @param retention is a list of specifications: each will + * apply to either all kinds, or a subset of kinds. Ranges + * may be specified for the kind field as a tuple of + * inclusive start and end values. Events of indicated kind + * (or all) are then limited to a `count` and/or time + * period. + + * It is possible to effectively blacklist Nostr-based + * protocols that rely on a specific `kind` number, by + * giving a retention time of zero for those `kind` values. + * While that is unfortunate, it does allow clients to + * discover servers that will support their protocol quickly + * via a single HTTP fetch. + + * There is no need to specify retention times for + * _ephemeral events_ as defined in [NIP-16](16.md) since + * they are not retained. + */ +export interface Retention { + retention: AnyRetentionDetails[] +} + +/** + * Some relays may be governed by the arbitrary laws of a + * nation state. This may limit what content can be stored + * in cleartext on those relays. All clients are encouraged + * to use encryption to work around this limitation. + + * It is not possible to describe the limitations of each + * country's laws and policies which themselves are + * typically vague and constantly shifting. + + * Therefore, this field allows the relay operator to + * indicate which countries' laws might end up being + * enforced on them, and then indirectly on their users' + * content. + + * Users should be able to avoid relays in countries they + * don't like, and/or select relays in more favourable + * zones. Exposing this flexibility is up to the client + * software. + + * @param relay_countries a list of two-level ISO country + * codes (ISO 3166-1 alpha-2) whose laws and policies may + * affect this relay. `EU` may be used for European Union + * countries. + + * Remember that a relay may be hosted in a country which is + * not the country of the legal entities who own the relay, + * so it's very likely a number of countries are involved. + */ +export interface ContentLimitations { + relay_countries: string[] +} + +/** + * ### Community Preferences + + * For public text notes at least, a relay may try to foster + * a local community. This would encourage users to follow + * the global feed on that relay, in addition to their usual + * individual follows. To support this goal, relays MAY + * specify some of the following values. + + * @param language_tags is an ordered list of [IETF + * language + * tags](https://en.wikipedia.org/wiki/IETF_language_tag + * indicating the major languages spoken on the relay. + * @param tags is a list of limitations on the topics to be + * discussed. For example `sfw-only` indicates that only + * "Safe For Work" content is encouraged on this relay. This + * relies on assumptions of what the "work" "community" + * feels "safe" talking about. In time, a common set of tags + * may emerge that allow users to find relays that suit + * their needs, and client software will be able to parse + * these tags easily. The `bitcoin-only` tag indicates that + * any _altcoin_, _"crypto"_ or _blockchain_ comments will + * be ridiculed without mercy. + * @param posting_policy is a link to a human-readable page + * which specifies the community policies for the relay. In + * cases where `sfw-only` is True, it's important to link to + * a page which gets into the specifics of your posting + * policy. + + * The `description` field should be used to describe your + * community goals and values, in brief. The + * `posting_policy` is for additional detail and legal + * terms. Use the `tags` field to signify limitations on + * content, or topics to be discussed, which could be + * machine processed by appropriate client software. + */ +export interface CommunityPreferences { + language_tags: string[] + tags: string[] + posting_policy: string +} + +export interface Amount { + amount: number + unit: 'msat' +} +export interface PublicationAmount extends Amount { + kinds: number[] +} +export interface Subscription extends Amount { + period: number +} +export interface Fees { + admission: Amount[] + subscription: Subscription[] + publication: PublicationAmount[] +} +/** + * Relays that require payments may want to expose their fee + * schedules. + */ +export interface PayToRelay { + payments_url: string + fees: Fees +} + +/** + * A URL pointing to an image to be used as an icon for the + * relay. Recommended to be squared in shape. + */ +export interface Icon { + icon: string +} + +export type RelayInfos< + N extends string[], + A extends boolean, + P extends boolean +> = RelayInfosTemplate & + Partial & { + limitation?: Partial> + } & Partial & + Partial & + Partial & + Partial