Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/v0.0.1 alpha.175 #163

Merged
merged 11 commits into from
Oct 30, 2024
15 changes: 14 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.2.2",
"lodash": "^4.17.21",
Expand All @@ -53,7 +54,9 @@
"proper-lockfile": "^4.1.2",
"superagent": "^10.1.0",
"redis": "^4.7.0",
"unzipper": "^0.12.3"
"superagent": "^10.0.0",
"unzipper": "^0.12.3",
"urns": "^0.6.0"
},
"devDependencies": {
"@types/archiver": "^6.0.2",
Expand Down
141 changes: 141 additions & 0 deletions src/utils/Udi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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 formatted as follows:
// urn:dig:chainName:storeId:rootHash/resourceKey
// The UDI can be used to uniquely identify resources across the DIG network.
//
class Udi {
readonly chainName: string;
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 | 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 = Udi.convertToBuffer(storeId);
this.rootHash = rootHash ? Udi.convertToBuffer(rootHash) : null;
this.resourceKey = resourceKey;
}

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);
}

withResourceKey(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 nid: ${parsedUrn.nid}`);
}

const parts = parsedUrn.nss.split(':');
if (parts.length < 2) {
throw new Error(`Invalid UDI format: ${parsedUrn.nss}`);
}

const chainName = parts[0];
const storeId = parts[1].split('/')[0];

let rootHash: string | null = null;
if (parts.length > 2) {
rootHash = parts[2].split('/')[0];
}

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(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) {
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.equals(other.storeId) &&
this.chainName === other.chainName &&
(this.rootHash && other.rootHash ? this.rootHash.equals(other.rootHash) : this.rootHash === other.rootHash) &&
this.resourceKey === other.resourceKey
);
}

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 };
68 changes: 68 additions & 0 deletions tests/udi.tests.ts
Original file line number Diff line number Diff line change
@@ -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 withRootHash', () => {
const udi = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1');
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 withResourceKey', () => {
const udi = new Udi('chia', 'store1', 'rootHash1', 'resourceKey1');
const newUdi = udi.withResourceKey('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');
});
});
Loading