From e93ed05c934f6d725e71ac5289fe50eef4127bc7 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Wed, 30 Oct 2024 00:43:01 +0000 Subject: [PATCH 1/7] add Udi class nad urns package dependency --- package-lock.json | 9 ++++- package.json | 3 +- src/utils/Udi.ts | 100 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 src/utils/Udi.ts diff --git a/package-lock.json b/package-lock.json index 91b846f..2e536fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,8 @@ "proper-lockfile": "^4.1.2", "redis": "^4.7.0", "superagent": "^10.0.0", - "unzipper": "^0.12.3" + "unzipper": "^0.12.3", + "urns": "^0.6.0" }, "devDependencies": { "@types/archiver": "^6.0.2", @@ -5463,6 +5464,12 @@ "node-int64": "^0.4.0" } }, + "node_modules/urns": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/urns/-/urns-0.6.0.tgz", + "integrity": "sha512-KqXGkRiq76KDvw+wHusJL0fSVltnF3Teqf1BK4f1xK3p1u1NAYYBQRsP89nw5CV/y+egjehITVPLh6upfqFdLg==", + "license": "MIT" + }, "node_modules/utf8": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", diff --git a/package.json b/package.json index dbaa14a..10d7714 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,8 @@ "proper-lockfile": "^4.1.2", "redis": "^4.7.0", "superagent": "^10.0.0", - "unzipper": "^0.12.3" + "unzipper": "^0.12.3", + "urns": "^0.6.0" }, "devDependencies": { "@types/archiver": "^6.0.2", diff --git a/src/utils/Udi.ts b/src/utils/Udi.ts new file mode 100644 index 0000000..29f14bd --- /dev/null +++ b/src/utils/Udi.ts @@ -0,0 +1,100 @@ +import * as urns from 'urns'; + +// +// This class encapsulates the concept of a Universal Data Identifier (UDI) which is a +// standardized way to identify resources across the distributed DIG mesh netowrk. +// The UDI is a URN (Uniform Resource Name) that is used to identify resources +// in the DIG network. The UDI is composed of the following parts: +// - Chain Name: The name of the blockchain network where the resource is stored. +// - Store ID: The unique identifier of the store where the resource is stored. +// - Root Hash: The root hash of the resource in the store. +// - Resource Key: The key of the resource in the store. +// The UDI is formatted as follows: +// urn:dig:chainName:storeId:rootHash/resourceKey +// The chainName and storeId are required, while the rootHash and resourceKey are optional. +// The UDI can be used to uniquely identify resources across the DIG network. +// The UDI can be converted to a URN string and vice versa. +// https://github.com/DIG-Network/DIPS/blob/c6792331acf3c185ca87a8f4f847561d2b47fb31/DIPs/dip-0001.md +// +class Udi { + readonly chainName: string; + readonly storeId: string; + readonly rootHash: string | null; + readonly resourceKey: string | null; + static readonly nid: string = "dig"; + static readonly namespace: string = `urn:${Udi.nid}`; + + constructor(chainName: string, storeId: string, rootHash: string | null = null, resourceKey: string | null = null) { + this.chainName = chainName ?? "chia"; + this.storeId = storeId; + this.rootHash = rootHash; + this.resourceKey = resourceKey; + } + + fromRootHash(rootHash: string): Udi { + return new Udi(this.chainName, this.storeId, rootHash, this.resourceKey); + } + + fromResourceKey(resourceKey: string | null): Udi { + return new Udi(this.chainName, this.storeId, this.rootHash, resourceKey); + } + + static fromUrn(urn: string): Udi { + const parsedUrn = urns.parseURN(urn); + if (parsedUrn.nid.toLowerCase() !== Udi.nid) { + throw new Error(`Invalid namespace: ${parsedUrn.nid}`); + } + + const parts = parsedUrn.nss.split(':'); + // at a minimum we need chain name and store id + if (parts.length < 2) { + throw new Error(`Invalid URN format: ${parsedUrn.nss}`); + } + + // this is what a nss looks like + //"chia:store id:optional_roothash/optional path/resource key" + const chainName = parts[0]; + const storeId = parts[1].split('/')[0]; // need to strip off the optional path component + + // root hash will always be the part after the second : + let rootHash: string | null = null; + if (parts.length > 2) { + rootHash = parts[2].split('/')[0]; // need to strip off the optional path component + } + + // now see if we have a path component which will always follow the first / + const pathParts = parsedUrn.nss.split('/'); + let resourceKey: string | null = null; + if (pathParts.length > 1) { + resourceKey = pathParts.slice(1).join('/'); + } + + return new Udi(chainName, storeId, rootHash, resourceKey); + } + + toUrn(): string { + let urn = `${Udi.namespace}:${this.chainName}:${this.storeId}`; + if (this.rootHash !== null) { + urn += `:${this.rootHash}`; + } + + if (this.resourceKey !== null) { + urn += `/${this.resourceKey}`; + } + + return urn; + } + + equals(other: Udi): boolean { + return this.storeId === other.storeId && + this.chainName === other.chainName && + this.rootHash === other.rootHash && + this.resourceKey === other.resourceKey; + } + + toString(): string { + return this.toUrn(); + } +} + +export { Udi }; \ No newline at end of file From e8ccb3949561baa6ad82edf9b06a3fce28289b4b Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Wed, 30 Oct 2024 00:52:39 +0000 Subject: [PATCH 2/7] add tests for Udi class --- tests/udi.tests.ts | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/udi.tests.ts diff --git a/tests/udi.tests.ts b/tests/udi.tests.ts new file mode 100644 index 0000000..0e62c4f --- /dev/null +++ b/tests/udi.tests.ts @@ -0,0 +1,68 @@ +import { expect } from 'chai'; +import { Udi } from '../src/utils/Udi'; + +describe('Udi', () => { + it('should initialize correctly with all parameters', () => { + const udi = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1'); + expect(udi.chainName).to.equal('chia'); + expect(udi.storeId).to.equal('store1'); + expect(udi.rootHash).to.equal('rootHash1'); + expect(udi.resourceKey).to.equal('resourceKey1'); + }); + + it('should initialize correctly with optional parameters', () => { + const udi = new Udi('chia', 'store1'); + expect(udi.chainName).to.equal('chia'); + expect(udi.storeId).to.equal('store1'); + expect(udi.rootHash).to.be.null; + expect(udi.resourceKey).to.be.null; + }); + + it('should create a new Udi with a different rootHash using fromRootHash', () => { + const udi = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1'); + const newUdi = udi.fromRootHash('newRootHash'); + expect(newUdi.rootHash).to.equal('newRootHash'); + expect(newUdi.resourceKey).to.equal('resourceKey1'); + }); + + it('should create a new Udi with a different resourceKey using fromResourceKey', () => { + const udi = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1'); + const newUdi = udi.fromResourceKey('newResourceKey'); + expect(newUdi.resourceKey).to.equal('newResourceKey'); + expect(newUdi.rootHash).to.equal('rootHash1'); + }); + + it('should create a Udi from a valid URN', () => { + const urn = 'urn:dig:chia:store1:rootHash1/resourceKey1'; + const udi = Udi.fromUrn(urn); + expect(udi.chainName).to.equal('chia'); + expect(udi.storeId).to.equal('store1'); + expect(udi.rootHash).to.equal('rootHash1'); + expect(udi.resourceKey).to.equal('resourceKey1'); + }); + + it('should throw an error for an invalid URN namespace', () => { + const urn = 'urn:invalid:chia:store1:rootHash1/resourceKey1'; + expect(() => Udi.fromUrn(urn)).to.throw('Invalid namespace: invalid'); + }); + + it('should convert a Udi to a URN string', () => { + const udi = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1'); + const urn = udi.toUrn(); + expect(urn).to.equal('urn:dig:chia:store1:rootHash1/resourceKey1'); + }); + + it('should correctly compare two Udi instances using equals', () => { + const udi1 = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1'); + const udi2 = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1'); + const udi3 = new Udi('chia', 'store2', 'rootHash1', 'resourceKey1'); + expect(udi1.equals(udi2)).to.be.true; + expect(udi1.equals(udi3)).to.be.false; + }); + + it('should convert a Udi to a string using toString', () => { + const udi = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1'); + const str = udi.toString(); + expect(str).to.equal('urn:dig:chia:store1:rootHash1/resourceKey1'); + }); +}); \ No newline at end of file From 29d974ece4c0070128f920809a3f7db2e9da3b24 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Wed, 30 Oct 2024 02:50:34 +0000 Subject: [PATCH 3/7] change name of from methods to be more typescripty --- src/utils/Udi.ts | 6 +++--- tests/udi.tests.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/utils/Udi.ts b/src/utils/Udi.ts index 29f14bd..f0f18a6 100644 --- a/src/utils/Udi.ts +++ b/src/utils/Udi.ts @@ -2,7 +2,7 @@ import * as urns from 'urns'; // // This class encapsulates the concept of a Universal Data Identifier (UDI) which is a -// standardized way to identify resources across the distributed DIG mesh netowrk. +// standardized way to identify resources across the distributed DIG mesh network. // The UDI is a URN (Uniform Resource Name) that is used to identify resources // in the DIG network. The UDI is composed of the following parts: // - Chain Name: The name of the blockchain network where the resource is stored. @@ -31,11 +31,11 @@ class Udi { this.resourceKey = resourceKey; } - fromRootHash(rootHash: string): Udi { + withRootHash(rootHash: string): Udi { return new Udi(this.chainName, this.storeId, rootHash, this.resourceKey); } - fromResourceKey(resourceKey: string | null): Udi { + withResourceKey(resourceKey: string | null): Udi { return new Udi(this.chainName, this.storeId, this.rootHash, resourceKey); } diff --git a/tests/udi.tests.ts b/tests/udi.tests.ts index 0e62c4f..1e5f42c 100644 --- a/tests/udi.tests.ts +++ b/tests/udi.tests.ts @@ -18,16 +18,16 @@ describe('Udi', () => { expect(udi.resourceKey).to.be.null; }); - it('should create a new Udi with a different rootHash using fromRootHash', () => { + it('should create a new Udi with a different rootHash using withRootHash', () => { const udi = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1'); - const newUdi = udi.fromRootHash('newRootHash'); + const newUdi = udi.withRootHash('newRootHash'); expect(newUdi.rootHash).to.equal('newRootHash'); expect(newUdi.resourceKey).to.equal('resourceKey1'); }); - it('should create a new Udi with a different resourceKey using fromResourceKey', () => { + it('should create a new Udi with a different resourceKey using withResourceKey', () => { const udi = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1'); - const newUdi = udi.fromResourceKey('newResourceKey'); + const newUdi = udi.withResourceKey('newResourceKey'); expect(newUdi.resourceKey).to.equal('newResourceKey'); expect(newUdi.rootHash).to.equal('rootHash1'); }); From 652ece254a1c6c086417820041f29fe17b50aa05 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Wed, 30 Oct 2024 02:54:14 +0000 Subject: [PATCH 4/7] allow roothash to be null in withRootHash --- src/utils/Udi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/Udi.ts b/src/utils/Udi.ts index f0f18a6..812fb0d 100644 --- a/src/utils/Udi.ts +++ b/src/utils/Udi.ts @@ -31,7 +31,7 @@ class Udi { this.resourceKey = resourceKey; } - withRootHash(rootHash: string): Udi { + withRootHash(rootHash: string | null): Udi { return new Udi(this.chainName, this.storeId, rootHash, this.resourceKey); } From 94db98e73dc420b52c85c94ab70bdb6c69b733a3 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Wed, 30 Oct 2024 02:56:37 +0000 Subject: [PATCH 5/7] use || instead of ?? to default chain name --- src/utils/Udi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/Udi.ts b/src/utils/Udi.ts index 812fb0d..f817597 100644 --- a/src/utils/Udi.ts +++ b/src/utils/Udi.ts @@ -25,7 +25,7 @@ class Udi { static readonly namespace: string = `urn:${Udi.nid}`; constructor(chainName: string, storeId: string, rootHash: string | null = null, resourceKey: string | null = null) { - this.chainName = chainName ?? "chia"; + this.chainName = chainName || "chia"; this.storeId = storeId; this.rootHash = rootHash; this.resourceKey = resourceKey; From fa98a37cd8e8723529e3ee9c4c0c6480936a7a9f Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Wed, 30 Oct 2024 03:10:21 +0000 Subject: [PATCH 6/7] gold plating --- src/utils/Udi.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/utils/Udi.ts b/src/utils/Udi.ts index f817597..74f3dc2 100644 --- a/src/utils/Udi.ts +++ b/src/utils/Udi.ts @@ -1,4 +1,5 @@ import * as urns from 'urns'; +import { createHash } from 'crypto'; // // This class encapsulates the concept of a Universal Data Identifier (UDI) which is a @@ -25,6 +26,9 @@ class Udi { static readonly namespace: string = `urn:${Udi.nid}`; constructor(chainName: string, storeId: string, rootHash: string | null = null, resourceKey: string | null = null) { + if (!storeId) { + throw new Error("storeId cannot be empty"); + } this.chainName = chainName || "chia"; this.storeId = storeId; this.rootHash = rootHash; @@ -42,13 +46,13 @@ class Udi { static fromUrn(urn: string): Udi { const parsedUrn = urns.parseURN(urn); if (parsedUrn.nid.toLowerCase() !== Udi.nid) { - throw new Error(`Invalid namespace: ${parsedUrn.nid}`); + throw new Error(`Invalid nid: ${parsedUrn.nid}`); } const parts = parsedUrn.nss.split(':'); // at a minimum we need chain name and store id if (parts.length < 2) { - throw new Error(`Invalid URN format: ${parsedUrn.nss}`); + throw new Error(`Invalid UDI format: ${parsedUrn.nss}`); } // this is what a nss looks like @@ -95,6 +99,16 @@ class Udi { toString(): string { return this.toUrn(); } + + clone(): Udi { + return new Udi(this.chainName, this.storeId, this.rootHash, this.resourceKey); + } + + hashCode(): string { + const hash = createHash('sha256'); + hash.update(this.toUrn()); + return hash.digest('hex'); + } } export { Udi }; \ No newline at end of file From bd6d6da48c263da07c1b5ff9964dd246dbfb08b9 Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Wed, 30 Oct 2024 10:32:24 -0400 Subject: [PATCH 7/7] feat: UDI class supports both base16 and base32 strings --- package-lock.json | 9 +++-- package.json | 1 + src/utils/Udi.ts | 89 ++++++++++++++++++++++++++++++----------------- 3 files changed, 66 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7a4d567..1dfb633 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "crypto-js": "^4.2.0", "figures": "^6.1.0", "fs-extra": "^11.2.0", + "hi-base32": "^0.5.1", "ignore": "^5.3.2", "inquirer": "^10.1.8", "lodash": "^4.17.21", @@ -3139,6 +3140,11 @@ "node": ">=8" } }, + "node_modules/hi-base32": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/hi-base32/-/hi-base32-0.5.1.tgz", + "integrity": "sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==" + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -5467,8 +5473,7 @@ "node_modules/urns": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/urns/-/urns-0.6.0.tgz", - "integrity": "sha512-KqXGkRiq76KDvw+wHusJL0fSVltnF3Teqf1BK4f1xK3p1u1NAYYBQRsP89nw5CV/y+egjehITVPLh6upfqFdLg==", - "license": "MIT" + "integrity": "sha512-KqXGkRiq76KDvw+wHusJL0fSVltnF3Teqf1BK4f1xK3p1u1NAYYBQRsP89nw5CV/y+egjehITVPLh6upfqFdLg==" }, "node_modules/utf8": { "version": "3.0.0", diff --git a/package.json b/package.json index 9c3e89e..12b72d7 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "crypto-js": "^4.2.0", "figures": "^6.1.0", "fs-extra": "^11.2.0", + "hi-base32": "^0.5.1", "ignore": "^5.3.2", "inquirer": "^10.1.8", "lodash": "^4.17.21", diff --git a/src/utils/Udi.ts b/src/utils/Udi.ts index 74f3dc2..cf662a0 100644 --- a/src/utils/Udi.ts +++ b/src/utils/Udi.ts @@ -1,41 +1,62 @@ import * as urns from 'urns'; import { createHash } from 'crypto'; +import { encode as base32Encode, decode as base32Decode } from 'hi-base32'; // // This class encapsulates the concept of a Universal Data Identifier (UDI) which is a // standardized way to identify resources across the distributed DIG mesh network. -// The UDI is a URN (Uniform Resource Name) that is used to identify resources -// in the DIG network. The UDI is composed of the following parts: -// - Chain Name: The name of the blockchain network where the resource is stored. -// - Store ID: The unique identifier of the store where the resource is stored. -// - Root Hash: The root hash of the resource in the store. -// - Resource Key: The key of the resource in the store. // The UDI is formatted as follows: // urn:dig:chainName:storeId:rootHash/resourceKey -// The chainName and storeId are required, while the rootHash and resourceKey are optional. // The UDI can be used to uniquely identify resources across the DIG network. -// The UDI can be converted to a URN string and vice versa. -// https://github.com/DIG-Network/DIPS/blob/c6792331acf3c185ca87a8f4f847561d2b47fb31/DIPs/dip-0001.md // class Udi { readonly chainName: string; - readonly storeId: string; - readonly rootHash: string | null; + readonly storeId: Buffer; + readonly rootHash: Buffer | null; readonly resourceKey: string | null; static readonly nid: string = "dig"; static readonly namespace: string = `urn:${Udi.nid}`; - constructor(chainName: string, storeId: string, rootHash: string | null = null, resourceKey: string | null = null) { + constructor( + chainName: string, + storeId: string | Buffer, + rootHash: string | Buffer | null = null, + resourceKey: string | null = null + ) { if (!storeId) { throw new Error("storeId cannot be empty"); } this.chainName = chainName || "chia"; - this.storeId = storeId; - this.rootHash = rootHash; + this.storeId = Udi.convertToBuffer(storeId); + this.rootHash = rootHash ? Udi.convertToBuffer(rootHash) : null; this.resourceKey = resourceKey; } - withRootHash(rootHash: string | null): Udi { + static convertToBuffer(input: string | Buffer): Buffer { + if (Buffer.isBuffer(input)) { + return input; + } + + if (Udi.isHex(input)) { + return Buffer.from(input, 'hex'); + } + + if (Udi.isBase32(input)) { + return Buffer.from(base32Decode(input, false)); // Decode as UTF-8 + } + + throw new Error("Invalid input encoding. Must be 32-byte hex or Base32 string."); + } + + static isHex(input: string): boolean { + return /^[a-fA-F0-9]{64}$/.test(input); + } + + static isBase32(input: string): boolean { + return /^[a-z2-7]{52}$/.test(input.toLowerCase()); + } + + withRootHash(rootHash: string | Buffer | null): Udi { return new Udi(this.chainName, this.storeId, rootHash, this.resourceKey); } @@ -50,23 +71,18 @@ class Udi { } const parts = parsedUrn.nss.split(':'); - // at a minimum we need chain name and store id if (parts.length < 2) { throw new Error(`Invalid UDI format: ${parsedUrn.nss}`); } - // this is what a nss looks like - //"chia:store id:optional_roothash/optional path/resource key" const chainName = parts[0]; - const storeId = parts[1].split('/')[0]; // need to strip off the optional path component + const storeId = parts[1].split('/')[0]; - // root hash will always be the part after the second : let rootHash: string | null = null; if (parts.length > 2) { - rootHash = parts[2].split('/')[0]; // need to strip off the optional path component + rootHash = parts[2].split('/')[0]; } - // now see if we have a path component which will always follow the first / const pathParts = parsedUrn.nss.split('/'); let resourceKey: string | null = null; if (pathParts.length > 1) { @@ -76,24 +92,35 @@ class Udi { return new Udi(chainName, storeId, rootHash, resourceKey); } - toUrn(): string { - let urn = `${Udi.namespace}:${this.chainName}:${this.storeId}`; - if (this.rootHash !== null) { - urn += `:${this.rootHash}`; + toUrn(encoding: 'hex' | 'base32' = 'hex'): string { + const storeIdStr = this.bufferToString(this.storeId, encoding); + let urn = `${Udi.namespace}:${this.chainName}:${storeIdStr}`; + + if (this.rootHash) { + const rootHashStr = this.bufferToString(this.rootHash, encoding); + urn += `:${rootHashStr}`; } - if (this.resourceKey !== null) { + if (this.resourceKey) { urn += `/${this.resourceKey}`; } return urn; } + bufferToString(buffer: Buffer, encoding: 'hex' | 'base32'): string { + return encoding === 'hex' + ? buffer.toString('hex') + : base32Encode(buffer).toLowerCase().replace(/=+$/, ''); + } + equals(other: Udi): boolean { - return this.storeId === other.storeId && + return ( + this.storeId.equals(other.storeId) && this.chainName === other.chainName && - this.rootHash === other.rootHash && - this.resourceKey === other.resourceKey; + (this.rootHash && other.rootHash ? this.rootHash.equals(other.rootHash) : this.rootHash === other.rootHash) && + this.resourceKey === other.resourceKey + ); } toString(): string { @@ -111,4 +138,4 @@ class Udi { } } -export { Udi }; \ No newline at end of file +export { Udi };