From 3ad0ad5fdffbc8ff6e7eb4a84f94e16d74075d64 Mon Sep 17 00:00:00 2001 From: ohager Date: Thu, 31 Aug 2023 11:22:04 -0300 Subject: [PATCH] feat: adjusted src47 URI Resolver to follow reviewed SRC47 spec --- packages/standards/src/src47/URIResolver.ts | 63 +++++- .../src/src47/__tests__/URIResolver.spec.ts | 208 ++++++++++-------- 2 files changed, 169 insertions(+), 102 deletions(-) diff --git a/packages/standards/src/src47/URIResolver.ts b/packages/standards/src/src47/URIResolver.ts index f8e37a6b..ee9ad6b7 100644 --- a/packages/standards/src/src47/URIResolver.ts +++ b/packages/standards/src/src47/URIResolver.ts @@ -5,6 +5,30 @@ import {Ledger} from '@signumjs/core'; import {DescriptorData} from '../src44'; +// extend this list from time to time... +export const KnownTlds = [ + 'blockchain', + 'coin', + 'crypto', + 'dao', + 'decentral', + 'dex', + 'free', + 'nft', + 'p2p', + 'signum', + 'signa', + 'sig', + 'sns', + 'w3', + 'wallet', + 'web3', + 'x', + 'y', + 'z', + 'nostr', // bought custom tld +]; + /** * @ignore */ @@ -19,25 +43,28 @@ interface URI { /** * URI Resolver * - * Resolves SRC47 compliant URIs via Signums Alias system to URLs + * Resolves SRC47 compliant URIs via Signums Alias system to URLs, or other internal fields * * ```ts * const resolver = new URIResolver(ledger); - * const resolvedURL = await resolver.resolve("signum://arts.johndoe"); + * const resolvedURL = await resolver.resolve("http://arts.johndoe@signum"); * ``` * * with TLD * ```ts * const resolver = new URIResolver(ledger); - * const resolvedURL = await resolver.resolve("signum://arts.johndoe:crypto"); + * const resolvedURL = await resolver.resolve("https://arts.johndoe@crypto"); * ``` * * Or get the account Id (if set) * * ```ts * const resolver = new URIResolver(ledger); - * const accountId = await resolver.resolve("signum://arts.johndoe/ac"); + * const accountId = await resolver.resolve("https://arts.johndoe@signum/ac"); * ``` + * + * Also considers known top-level domains and accepts the following format for those URIs: .. (instead of `@`) + * * @module standards.SRC47 */ export class URIResolver { @@ -51,7 +78,9 @@ export class URIResolver { * @throws Error if URI is not compliant */ public static parseURI(uri: string): URI { - const regex = /^(?http|https|signum):\/\/(?\$?[\w.]+?)(:(?\w+)?)?(\/(?[\w-]+)?)?$/gm; + + uri = URIResolver.convertKnownTldUri(uri); + const regex = /^(?http|https):\/\/(?\$?[\w.]+?)(@(?\w+)?)?(\/(?[\w-]+)?)?$/gm; const result = regex.exec(uri.toLowerCase()); // @ts-ignore @@ -86,6 +115,30 @@ export class URIResolver { }; } + /** + * Converts the given URI to a known top-level domain (TLD) format. + * + * @param {string} uri - The URI to be converted. + * @return {string} - The converted URI with a known TLD format. + */ + public static convertKnownTldUri(uri: string): string { + uri = uri.toLowerCase(); + for (const knownTld of KnownTlds) { + const index = uri.lastIndexOf('.' + knownTld); + if (index > -1) { + const pathIndex = uri.indexOf('/', index); + const path = pathIndex > -1 ? uri.substring(pathIndex) : ''; + uri = uri.slice(0, index) + `@${knownTld}`; + if(path){ + uri += path; + } + break; + } + } + + return uri; + } + /** * Tries to resolve the URI * @param uri A compliant URI diff --git a/packages/standards/src/src47/__tests__/URIResolver.spec.ts b/packages/standards/src/src47/__tests__/URIResolver.spec.ts index 2247b38c..36c6878c 100644 --- a/packages/standards/src/src47/__tests__/URIResolver.spec.ts +++ b/packages/standards/src/src47/__tests__/URIResolver.spec.ts @@ -1,4 +1,4 @@ -import {URIResolver} from '../URIResolver'; +import {KnownTlds, URIResolver} from '../URIResolver'; const TestAliases = { 'johndoe': { @@ -7,33 +7,36 @@ const TestAliases = { 'john_doe': { aliasURI: JSON.stringify({vs: 1, hp: 'https://johndoe.com', ac: '1234567891011121314', 'x-custom': {foo: 'bar'}}) }, - 'johndoe:signum': { + 'johndoe@signum': { aliasURI: JSON.stringify({vs: 1, hp: 'https://johndoe.com', ac: '1234567891011121314', 'x-custom': {foo: 'bar'}}) }, - 'johndoe:crypto': { + 'johndoe@crypto': { aliasURI: JSON.stringify({vs: 1, hp: 'https://johndoe.com', ac: '1234567891011121314', 'x-custom': {foo: 'bar'}}) }, - 'johndoe:web3': { + 'johndoe@web3': { aliasURI: JSON.stringify({vs: 1, hp: 'https://johndoe.com', ac: '1234567891011121314', 'x-custom': {foo: 'bar'}}) }, // single hop 'johndoe1': { aliasURI: JSON.stringify({vs: 1, hp: 'https://johndoe.com', al: 'jd00001:web3'}) }, - 'johndoe1:crypto': { + 'johndoe1@crypto': { aliasURI: JSON.stringify({vs: 1, hp: 'https://johndoe.com', al: 'jd00001:web3'}) }, - 'jd00001:web3': { + 'jd00001@web3': { aliasURI: JSON.stringify({vs: 1, nm: 'arts', hp: 'https://signumart.io/profile/123456'}) }, // multi hop 'johndoe2': { aliasURI: JSON.stringify({vs: 1, hp: 'https://johndoe.com', al: 'jd00002'}) }, - 'johndoe2:crypto': { + 'johndoe2@signum': { aliasURI: JSON.stringify({vs: 1, hp: 'https://johndoe.com', al: 'jd00002'}) }, - 'johndoe2:web3': { + 'johndoe2@crypto': { + aliasURI: JSON.stringify({vs: 1, hp: 'https://johndoe.com', al: 'jd00002'}) + }, + 'johndoe2@web3': { aliasURI: JSON.stringify({vs: 1, hp: 'https://johndoe.com', al: 'jd00002'}) }, 'jd00002': { @@ -71,7 +74,7 @@ const TestAliases = { export const MockLedger = { alias: { getAliasByName: (name: string, tld?: string) => { - const alias = tld ? TestAliases[`${name}:${tld}`] : TestAliases[name]; + const alias = tld ? TestAliases[`${name}@${tld}`] : TestAliases[name]; // usually it returns an HttpError, but we mock that here return !alias ? Promise.reject('Unknown alias') : Promise.resolve(alias); } @@ -79,43 +82,38 @@ export const MockLedger = { }; describe('URIResolver', () => { + + describe('resolve', () => { describe('single domain', () => { - it('should resolve by signum:// scheme', async () => { + it('should resolve by TLDs', async () => { // @ts-ignore const resolver = new URIResolver(MockLedger); - const url = await resolver.resolve('signum://johndoe'); + let url = await resolver.resolve('http://johndoe.crypto'); expect(url).toEqual('https://johndoe.com'); - }); - it('should resolve by signum:// scheme and alias with `_`', async () => { - // @ts-ignore - const resolver = new URIResolver(MockLedger); - const url = await resolver.resolve('signum://john_doe'); + url = await resolver.resolve('https://johndoe.signum'); expect(url).toEqual('https://johndoe.com'); - }); - it('should resolve by shortcut \"$\"', async () => { - // @ts-ignore - const resolver = new URIResolver(MockLedger); - const url = await resolver.resolve('http://$johndoe'); + url = await resolver.resolve('https://johndoe.web3'); expect(url).toEqual('https://johndoe.com'); + // and all the other domains }); - it('should resolve by TLDs', async () => { + it('should resolve by TLDs using `@`', async () => { // @ts-ignore const resolver = new URIResolver(MockLedger); - let url = await resolver.resolve('http://johndoe:crypto'); + let url = await resolver.resolve('http://johndoe@crypto'); expect(url).toEqual('https://johndoe.com'); - url = await resolver.resolve('https://johndoe:signum'); + url = await resolver.resolve('https://johndoe@signum'); expect(url).toEqual('https://johndoe.com'); - url = await resolver.resolve('https://johndoe:web3'); + url = await resolver.resolve('https://johndoe@web3'); expect(url).toEqual('https://johndoe.com'); // and all the other domains }); it('should resolve deeply', async () => { // @ts-ignore const resolver = new URIResolver(MockLedger); - const accountId = await resolver.resolve('signum://johndoe/ac'); + const accountId = await resolver.resolve('http://johndoe.signum/ac'); expect(accountId).toEqual('1234567891011121314'); - const custom = await resolver.resolve('signum://johndoe/x-custom'); + const custom = await resolver.resolve('https://johndoe.signum/x-custom'); expect(custom).toEqual({foo: 'bar'}); }); } @@ -124,29 +122,25 @@ describe('URIResolver', () => { it('should resolve by subdomain (with tld) - single hop', async () => { // @ts-ignore const resolver = new URIResolver(MockLedger); - let url = await resolver.resolve('http://arts.johndoe1:crypto'); - expect(url).toEqual('https://signumart.io/profile/123456'); - url = await resolver.resolve('http://$arts.johndoe1'); + const url = await resolver.resolve('http://arts.johndoe1.crypto'); expect(url).toEqual('https://signumart.io/profile/123456'); }); it('should resolve by subdomain - multi hop', async () => { // @ts-ignore const resolver = new URIResolver(MockLedger); - let url = await resolver.resolve('http://arts.johndoe2:crypto'); - expect(url).toEqual('https://signumart.io/profile/123456'); - url = await resolver.resolve('http://$arts.johndoe2'); + let url = await resolver.resolve('http://arts.johndoe2.crypto'); expect(url).toEqual('https://signumart.io/profile/123456'); - url = await resolver.resolve('http://social.johndoe2:web3'); - expect(url).toEqual('https://twitter.com/jd1337'); - url = await resolver.resolve('http://$social.johndoe2'); + url = await resolver.resolve('http://social.johndoe2@web3'); expect(url).toEqual('https://twitter.com/jd1337'); }); it('should resolve deeply', async () => { // @ts-ignore const resolver = new URIResolver(MockLedger); - const accountId = await resolver.resolve('signum://johndoe/ac'); + const accountId = await resolver.resolve('https://johndoe.signum/ac'); expect(accountId).toEqual('1234567891011121314'); - const custom = await resolver.resolve('http://$social.johndoe2/x-custom'); + let custom = await resolver.resolve('http://social.johndoe2/x-custom'); + expect(custom).toEqual({foo: 'bar'}); + custom = await resolver.resolve('http://social.johndoe2@signum/x-custom'); expect(custom).toEqual({foo: 'bar'}); }); } @@ -157,40 +151,40 @@ describe('URIResolver', () => { try { // @ts-ignore const resolver = new URIResolver(MockLedger); - await resolver.resolve('http://janedoe:crypto'); + await resolver.resolve('http://janedoe.crypto'); fail('Expect exception'); } catch (e) { - expect(e.message).toMatch('Could not resolve: http://janedoe:crypto'); + expect(e.message).toMatch('Could not resolve: http://janedoe.crypto'); } }); it('cannot find the subdomain', async () => { try { // @ts-ignore const resolver = new URIResolver(MockLedger); - await resolver.resolve('http://weird.johndoe1:x'); + await resolver.resolve('http://weird.johndoe1.x'); fail('Expect exception'); } catch (e) { - expect(e.message).toMatch('Could not resolve: http://weird.johndoe1:x'); + expect(e.message).toMatch('Could not resolve: http://weird.johndoe1.x'); } }); it('stops circular dependency - to initial domain alias', async () => { try { // @ts-ignore const resolver = new URIResolver(MockLedger); - await resolver.resolve('http://weird.johndoe3:x'); + await resolver.resolve('http://weird.johndoe3.x'); fail('Expect exception'); } catch (e) { - expect(e.message).toMatch('Could not resolve: http://weird.johndoe3:x'); + expect(e.message).toMatch('Could not resolve: http://weird.johndoe3.x'); } }); it('stops circular dependency - to some internal alias', async () => { try { // @ts-ignore const resolver = new URIResolver(MockLedger); - await resolver.resolve('http://weird.johndoe4:x'); + await resolver.resolve('http://weird.johndoe4.x'); fail('Expect exception'); } catch (e) { - expect(e.message).toMatch('Could not resolve: http://weird.johndoe4:x'); + expect(e.message).toMatch('Could not resolve: http://weird.johndoe4.x'); } }); it('invalid Uri - #1', async () => { @@ -227,10 +221,10 @@ describe('URIResolver', () => { try { // @ts-ignore const resolver = new URIResolver(MockLedger); - await resolver.resolve('signum://johndoe/invalid'); + await resolver.resolve('http://johndoe.signum/invalid'); fail('Expect exception'); } catch (e) { - expect(e.message).toMatch('Could not resolve: signum://johndoe/invalid'); + expect(e.message).toMatch('Could not resolve: http://johndoe.signum/invalid'); } }); @@ -239,89 +233,53 @@ describe('URIResolver', () => { } ); describe('parseURI', () => { - it('should resolve by signum:// scheme', () => { - expect(URIResolver.parseURI('signum://johndoe')).toEqual({ - schema: 'signum', - domain: 'johndoe' - }); - expect(URIResolver.parseURI('signum://sub.johndoe')).toEqual({ - schema: 'signum', - domain: 'johndoe', - subdomain: 'sub' - }); - }); - it('should resolve by shortcut \"$\"', () => { - expect(URIResolver.parseURI('http://$johndoe')).toEqual({ - schema: 'http', - domain: 'johndoe' - }); - expect(URIResolver.parseURI('https://$johndoe')).toEqual({ - schema: 'https', - domain: 'johndoe' - }); - // for robustness - expect(URIResolver.parseURI('signum://$johndoe')).toEqual({ - schema: 'signum', - domain: 'johndoe' - }); - expect(URIResolver.parseURI('http://$sub.johndoe')).toEqual({ - schema: 'http', - domain: 'johndoe', - subdomain: 'sub' - }); - expect(URIResolver.parseURI('https://$sub.johndoe')).toEqual({ - schema: 'https', - domain: 'johndoe', - subdomain: 'sub' - }); - }); it('should resolve by TLDs', () => { - expect(URIResolver.parseURI('http://johndoe:signum')).toEqual({ + expect(URIResolver.parseURI('http://johndoe.signum')).toEqual({ schema: 'http', domain: 'johndoe', tld: 'signum', }); - expect(URIResolver.parseURI('http://sub.johndoe:signum')).toEqual({ + expect(URIResolver.parseURI('http://sub.johndoe.signum')).toEqual({ schema: 'http', domain: 'johndoe', subdomain: 'sub', tld: 'signum' }); - expect(URIResolver.parseURI('http://johndoe:signa')).toEqual({ + expect(URIResolver.parseURI('http://johndoe@signa')).toEqual({ schema: 'http', domain: 'johndoe', tld: 'signa', }); - expect(URIResolver.parseURI('http://sub.johndoe:signum')).toEqual({ + expect(URIResolver.parseURI('http://sub.johndoe.signum')).toEqual({ schema: 'http', domain: 'johndoe', subdomain: 'sub', tld: 'signum' }); - expect(URIResolver.parseURI('http://johndoe:free')).toEqual({ + expect(URIResolver.parseURI('http://johndoe@free')).toEqual({ schema: 'http', domain: 'johndoe', tld: 'free', }); - expect(URIResolver.parseURI('http://sub.johndoe:crypto')).toEqual({ + expect(URIResolver.parseURI('http://sub.johndoe@crypto')).toEqual({ schema: 'http', domain: 'johndoe', subdomain: 'sub', tld: 'crypto' }); - expect(URIResolver.parseURI('http://sub.johndoe:sig')).toEqual({ + expect(URIResolver.parseURI('http://sub.johndoe.sig')).toEqual({ schema: 'http', domain: 'johndoe', subdomain: 'sub', tld: 'sig' }); - expect(URIResolver.parseURI('http://sub.johndoe:web3')).toEqual({ + expect(URIResolver.parseURI('http://sub.johndoe.web3')).toEqual({ schema: 'http', domain: 'johndoe', subdomain: 'sub', tld: 'web3' }); - expect(URIResolver.parseURI('http://sub.johndoe:customtld')).toEqual({ + expect(URIResolver.parseURI('http://sub.johndoe@customtld')).toEqual({ schema: 'http', domain: 'johndoe', subdomain: 'sub', @@ -330,11 +288,67 @@ describe('URIResolver', () => { }); it('should throw on unsupported scheme', () => { - expect(() => URIResolver.parseURI('mailto://sub.johndoe:web3')).toThrow('Invalid SRC47 URI: mailto://sub.johndoe:web3'); - }); - it('should throw on unsupported tld patterb', () => { - expect(() => URIResolver.parseURI('http://sub.johndoe.web3')).toThrow('Invalid SRC47 URI: http://sub.johndoe.web3'); + expect(() => URIResolver.parseURI('mailto://sub.johndoe.web3')).toThrow('Invalid SRC47 URI: mailto://sub.johndoe@web3'); }); } ); + + describe('convertKnownTldUri', () => { + it('should convert known TLD in the URI', () => { + for (const tld of KnownTlds) { + const uri = `http://subdomain.domain.${tld}`; + const convertedUri = URIResolver.convertKnownTldUri(uri); + expect(convertedUri).toEqual(`http://subdomain.domain@${tld}`); + } + }); + it('should convert known TLD in the URI', () => { + for (const tld of KnownTlds) { + const uri = `https://domain.${tld}`; + const convertedUri = URIResolver.convertKnownTldUri(uri); + expect(convertedUri).toEqual(`https://domain@${tld}`); + } + }); + + it('should not convert unknown TLD in the URI', () => { + const uri = 'http://example@org'; + const convertedUri = URIResolver.convertKnownTldUri(uri); + expect(convertedUri).toEqual('http://example@org'); + }); + it('should not convert unknown TLD in the URI - 2', () => { + const uri = 'http://example.org'; + const convertedUri = URIResolver.convertKnownTldUri(uri); + expect(convertedUri).toEqual('http://example.org'); + }); + + it('should handle case-insensitive TLDs', () => { + const uri = 'http://EXample@COM'; + const convertedUri = URIResolver.convertKnownTldUri(uri); + expect(convertedUri).toEqual('http://example@com'); + }); + + it('should handle append further (deep) field paths - domain only', () => { + for (const tld of KnownTlds) { + const uri = `https://domain.${tld}/ac`; + const convertedUri = URIResolver.convertKnownTldUri(uri); + expect(convertedUri).toEqual(`https://domain@${tld}/ac`); + } + }); + + it('should handle append further (deep) field paths - with subdomains', () => { + for (const tld of KnownTlds) { + const uri = `http://subdomain.domain.${tld}/ac`; + const convertedUri = URIResolver.convertKnownTldUri(uri); + expect(convertedUri).toEqual(`http://subdomain.domain@${tld}/ac`); + } + }); + + it('should handle append further (deep) field paths - with subdomains and @', () => { + for (const tld of KnownTlds) { + const uri = `http://subdomain.domain@${tld}/ac`; + const convertedUri = URIResolver.convertKnownTldUri(uri); + expect(convertedUri).toEqual(`http://subdomain.domain@${tld}/ac`); + } + }); + }); + });