From dcd1b00a1d54a60ebb7524a8540c2808a7157778 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 16 Apr 2025 22:02:47 +0530 Subject: [PATCH 1/8] feat: create w3id builder --- infrastructure/w3id/src/index.ts | 65 +++++- infrastructure/w3id/src/logs/log-manager.ts | 247 ++++++++++---------- infrastructure/w3id/src/logs/log.types.ts | 42 ++-- infrastructure/w3id/src/utils/rand.ts | 19 ++ infrastructure/w3id/tests/logs/log.test.ts | 17 +- 5 files changed, 237 insertions(+), 153 deletions(-) create mode 100644 infrastructure/w3id/src/utils/rand.ts diff --git a/infrastructure/w3id/src/index.ts b/infrastructure/w3id/src/index.ts index ff8b4c56..1ccfcc39 100644 --- a/infrastructure/w3id/src/index.ts +++ b/infrastructure/w3id/src/index.ts @@ -1 +1,64 @@ -export default {}; +/** + * According to the project specification there are supposed to be 2 main types of + * W3ID's ones which are tied to more tangible entities and hence need rotation + * and others which are just UUIDs. Hence the approach to do this in a builder pattern + */ + +import { IDLogManager } from "./logs/log-manager"; +import { LogEvent, Signer } from "./logs/log.types"; +import { StorageSpec } from "./logs/storage/storage-spec"; +import { generateRandomAlphaNum } from "./utils/rand"; +import { v4 as uuidv4 } from "uuid"; +import { generateUuid } from "./utils/uuid"; + +class W3ID { + constructor( + public id: string, + public logs?: IDLogManager, + ) {} +} + +export class W3IDBuilder { + private signer?: Signer; + private repository?: StorageSpec; + private entropy?: string; + private namespace?: string; + + public withEntropy(str: string): W3IDBuilder { + this.entropy = str; + return this; + } + + public withNamespace(uuid: string): W3IDBuilder { + this.namespace = uuid; + return this; + } + + public withRepository( + storage: StorageSpec, + ): W3IDBuilder { + this.repository = storage; + return this; + } + + public withSigner(signer: Signer): W3IDBuilder { + this.signer = signer; + return this; + } + + public build(): W3ID { + this.entropy = this.entropy ?? generateRandomAlphaNum(); + this.namespace = this.namespace ?? uuidv4(); + const id = generateUuid(this.entropy, this.namespace); + if (!this.signer) { + return new W3ID(id); + } else { + if (!this.repository) + throw new Error( + "Repository is required, pass with \`withRepository\` method", + ); + const logs = new IDLogManager(this.repository, this.signer); + return new W3ID(id, logs); + } + } +} diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index b69e3dc2..0b024c03 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -1,21 +1,22 @@ import canonicalize from "canonicalize"; import { - BadNextKeySpecifiedError, - BadOptionsSpecifiedError, - BadSignatureError, - MalformedHashChainError, - MalformedIndexChainError, + BadNextKeySpecifiedError, + BadOptionsSpecifiedError, + BadSignatureError, + MalformedHashChainError, + MalformedIndexChainError, } from "../errors/errors"; import { isSubsetOf } from "../utils/array"; import { hash } from "../utils/hash"; import { - isGenesisOptions, - isRotationOptions, - type CreateLogEventOptions, - type GenesisLogOptions, - type LogEvent, - type RotationLogOptions, - type VerifierCallback, + isGenesisOptions, + isRotationOptions, + type Signer, + type CreateLogEventOptions, + type GenesisLogOptions, + type LogEvent, + type RotationLogOptions, + type VerifierCallback, } from "./log.types"; import type { StorageSpec } from "./storage/storage-spec"; @@ -27,122 +28,132 @@ import type { StorageSpec } from "./storage/storage-spec"; // TODO: Create a specification link inside our docs for how generation of identifier works export class IDLogManager { - repository: StorageSpec; + repository: StorageSpec; + signer: Signer; - constructor(repository: StorageSpec) { - this.repository = repository; - } + constructor(repository: StorageSpec, signer: Signer) { + this.repository = repository; + this.signer = signer; + } - static async validateLogChain( - log: LogEvent[], - verifyCallback: VerifierCallback, - ) { - let currIndex = 0; - let currentNextKeyHashesSeen: string[] = []; - let lastUpdateKeysSeen: string[] = []; - let lastHash: string | null = null; + static async validateLogChain( + log: LogEvent[], + verifyCallback: VerifierCallback, + ) { + let currIndex = 0; + let currentNextKeyHashesSeen: string[] = []; + let lastUpdateKeysSeen: string[] = []; + let lastHash: string | null = null; - for (const e of log) { - const [_index, _hash] = e.versionId.split("-"); - const index = Number(_index); - if (currIndex !== index) throw new MalformedIndexChainError(); - const hashedUpdateKeys = await Promise.all( - e.updateKeys.map(async (k) => await hash(k)), - ); - if (index > 0) { - const updateKeysSeen = isSubsetOf( - hashedUpdateKeys, - currentNextKeyHashesSeen, - ); - if (!updateKeysSeen || lastHash !== _hash) - throw new MalformedHashChainError(); - } + for (const e of log) { + const [_index, _hash] = e.versionId.split("-"); + const index = Number(_index); + if (currIndex !== index) throw new MalformedIndexChainError(); + const hashedUpdateKeys = await Promise.all( + e.updateKeys.map(async (k) => await hash(k)), + ); + if (index > 0) { + const updateKeysSeen = isSubsetOf( + hashedUpdateKeys, + currentNextKeyHashesSeen, + ); + if (!updateKeysSeen || lastHash !== _hash) + throw new MalformedHashChainError(); + } - currentNextKeyHashesSeen = e.nextKeyHashes; - await IDLogManager.verifyLogEventProof( - e, - lastUpdateKeysSeen.length > 0 ? lastUpdateKeysSeen : e.updateKeys, - verifyCallback, - ); - lastUpdateKeysSeen = e.updateKeys; - currIndex++; - lastHash = await hash(canonicalize(e) as string); - } - return true; - } + currentNextKeyHashesSeen = e.nextKeyHashes; + await IDLogManager.verifyLogEventProof( + e, + lastUpdateKeysSeen.length > 0 + ? lastUpdateKeysSeen + : e.updateKeys, + verifyCallback, + ); + lastUpdateKeysSeen = e.updateKeys; + currIndex++; + lastHash = await hash(canonicalize(e) as string); + } + return true; + } - private static async verifyLogEventProof( - e: LogEvent, - currentUpdateKeys: string[], - verifyCallback: VerifierCallback, - ) { - const proof = e.proof; - const copy = JSON.parse(JSON.stringify(e)); - // biome-ignore lint/performance/noDelete: we need to delete proof completely - delete copy.proof; - const canonicalJson = canonicalize(copy); - let verified = false; - if (!proof) throw new BadSignatureError("No proof found in the log event."); - for (const key of currentUpdateKeys) { - const signValidates = await verifyCallback( - canonicalJson as string, - proof, - key, - ); - if (signValidates) verified = true; - } - if (!verified) throw new BadSignatureError(); - } + private static async verifyLogEventProof( + e: LogEvent, + currentUpdateKeys: string[], + verifyCallback: VerifierCallback, + ) { + const proof = e.proof; + const copy = JSON.parse(JSON.stringify(e)); + // biome-ignore lint/performance/noDelete: we need to delete proof completely + delete copy.proof; + const canonicalJson = canonicalize(copy); + let verified = false; + if (!proof) + throw new BadSignatureError("No proof found in the log event."); + for (const key of currentUpdateKeys) { + const signValidates = await verifyCallback( + canonicalJson as string, + proof, + key, + ); + if (signValidates) verified = true; + } + if (!verified) throw new BadSignatureError(); + } - private async appendEntry(entries: LogEvent[], options: RotationLogOptions) { - const { signer, nextKeyHashes, nextKeySigner } = options; - const latestEntry = entries[entries.length - 1]; - const logHash = await hash(latestEntry); - const index = Number(latestEntry.versionId.split("-")[0]) + 1; + private async appendEntry( + entries: LogEvent[], + options: RotationLogOptions, + ) { + const { nextKeyHashes, nextKeySigner } = options; + const latestEntry = entries[entries.length - 1]; + const logHash = await hash(latestEntry); + const index = Number(latestEntry.versionId.split("-")[0]) + 1; - const currKeyHash = await hash(nextKeySigner.pubKey); - if (!latestEntry.nextKeyHashes.includes(currKeyHash)) - throw new BadNextKeySpecifiedError(); + const currKeyHash = await hash(nextKeySigner.pubKey); + if (!latestEntry.nextKeyHashes.includes(currKeyHash)) + throw new BadNextKeySpecifiedError(); - const logEvent: LogEvent = { - id: latestEntry.id, - versionTime: new Date(Date.now()), - versionId: `${index}-${logHash}`, - updateKeys: [nextKeySigner.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; + const logEvent: LogEvent = { + id: latestEntry.id, + versionTime: new Date(Date.now()), + versionId: `${index}-${logHash}`, + updateKeys: [nextKeySigner.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; - const proof = await signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; + const proof = await this.signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } + await this.repository.create(logEvent); + this.signer = nextKeySigner; + return logEvent; + } - private async createGenesisEntry(options: GenesisLogOptions) { - const { id, nextKeyHashes, signer } = options; - const logEvent: LogEvent = { - id, - versionId: `0-${id.split("@")[1]}`, - versionTime: new Date(Date.now()), - updateKeys: [signer.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; - const proof = await signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } + private async createGenesisEntry(options: GenesisLogOptions) { + const { id, nextKeyHashes } = options; + const logEvent: LogEvent = { + id, + versionId: `0-${id.split("@")[1]}`, + versionTime: new Date(Date.now()), + updateKeys: [this.signer.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; + const proof = await this.signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; + await this.repository.create(logEvent); + return logEvent; + } - async createLogEvent(options: CreateLogEventOptions) { - const entries = await this.repository.findMany({}); - if (entries.length > 0) { - if (!isRotationOptions(options)) throw new BadOptionsSpecifiedError(); - return this.appendEntry(entries, options); - } - if (!isGenesisOptions(options)) throw new BadOptionsSpecifiedError(); - return this.createGenesisEntry(options); - } + async createLogEvent(options: CreateLogEventOptions) { + const entries = await this.repository.findMany({}); + if (entries.length > 0) { + if (!isRotationOptions(options)) + throw new BadOptionsSpecifiedError(); + return this.appendEntry(entries, options); + } + if (!isGenesisOptions(options)) throw new BadOptionsSpecifiedError(); + return this.createGenesisEntry(options); + } } diff --git a/infrastructure/w3id/src/logs/log.types.ts b/infrastructure/w3id/src/logs/log.types.ts index 2f03b311..39fa47af 100644 --- a/infrastructure/w3id/src/logs/log.types.ts +++ b/infrastructure/w3id/src/logs/log.types.ts @@ -1,45 +1,43 @@ export type LogEvent = { - id: string; - versionId: string; - versionTime: Date; - updateKeys: string[]; - nextKeyHashes: string[]; - method: `w3id:v${string}`; - proof?: string; + id: string; + versionId: string; + versionTime: Date; + updateKeys: string[]; + nextKeyHashes: string[]; + method: `w3id:v${string}`; + proof?: string; }; export type VerifierCallback = ( - message: string, - signature: string, - pubKey: string, + message: string, + signature: string, + pubKey: string, ) => Promise; export type Signer = { - sign: (message: string) => Promise | string; - pubKey: string; + sign: (message: string) => Promise | string; + pubKey: string; }; export type RotationLogOptions = { - nextKeyHashes: string[]; - signer: Signer; - nextKeySigner: Signer; + nextKeyHashes: string[]; + nextKeySigner: Signer; }; export type GenesisLogOptions = { - nextKeyHashes: string[]; - id: string; - signer: Signer; + nextKeyHashes: string[]; + id: string; }; export function isGenesisOptions( - options: CreateLogEventOptions, + options: CreateLogEventOptions, ): options is GenesisLogOptions { - return "id" in options; + return "id" in options; } export function isRotationOptions( - options: CreateLogEventOptions, + options: CreateLogEventOptions, ): options is RotationLogOptions { - return "nextKeySigner" in options; + return "nextKeySigner" in options; } export type CreateLogEventOptions = GenesisLogOptions | RotationLogOptions; diff --git a/infrastructure/w3id/src/utils/rand.ts b/infrastructure/w3id/src/utils/rand.ts new file mode 100644 index 00000000..bad43ea7 --- /dev/null +++ b/infrastructure/w3id/src/utils/rand.ts @@ -0,0 +1,19 @@ +/** + * Generate a random alphanumeric sequence with set length + * + * @param {number} length length of the alphanumeric string you want + * @returns {string} + */ + +export function generateRandomAlphaNum(length: number = 16): string { + const chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + const charsLength = chars.length; + + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * charsLength)); + } + + return result; +} diff --git a/infrastructure/w3id/tests/logs/log.test.ts b/infrastructure/w3id/tests/logs/log.test.ts index 90db3bbc..57576732 100644 --- a/infrastructure/w3id/tests/logs/log.test.ts +++ b/infrastructure/w3id/tests/logs/log.test.ts @@ -61,12 +61,14 @@ class InMemoryStorage ); } } -const logManager = new IDLogManager(InMemoryStorage.build()); -const w3id = `@${generateUuid("asdfa")}`; - const keyPair = nacl.sign.keyPair(); let currNextKey = nacl.sign.keyPair(); +const signer = createSigner(keyPair); + +const logManager = new IDLogManager(InMemoryStorage.build(), signer); +const w3id = `@${generateUuid("asdfa")}`; + const verifierCallback: VerifierCallback = async ( message: string, signature: string, @@ -104,18 +106,15 @@ describe("LogManager", async () => { const logEvent = logManager.createLogEvent({ nextKeySigner: signer, nextKeyHashes: [nextKeyHash], - signer, }); await expect(logEvent).rejects.toThrow(BadOptionsSpecifiedError); }); test("GenesisEvent: [Creates Entry]", async () => { const nextKeyHash = await hash(uint8ArrayToHex(currNextKey.publicKey)); - const signer = createSigner(keyPair); const logEvent = await logManager.createLogEvent({ id: w3id, nextKeyHashes: [nextKeyHash], - signer, }); expectTypeOf(logEvent).toMatchObjectType(); }); @@ -124,10 +123,8 @@ describe("LogManager", async () => { const nextKeyPair = nacl.sign.keyPair(); const nextKeyHash = await hash(uint8ArrayToHex(nextKeyPair.publicKey)); - const signer = createSigner(nextKeyPair); const logEvent = logManager.createLogEvent({ nextKeyHashes: [nextKeyHash], - signer, id: `@{falso.randUuid()}`, }); @@ -138,11 +135,9 @@ describe("LogManager", async () => { const nextKeyPair = nacl.sign.keyPair(); const nextKeyHash = await hash(uint8ArrayToHex(nextKeyPair.publicKey)); - const signer = createSigner(nextKeyPair); const nextKeySigner = createSigner(nextKeyPair); const logEvent = logManager.createLogEvent({ nextKeyHashes: [nextKeyHash], - signer, nextKeySigner, }); @@ -153,11 +148,9 @@ describe("LogManager", async () => { const nextKeyPair = nacl.sign.keyPair(); const nextKeyHash = await hash(uint8ArrayToHex(nextKeyPair.publicKey)); - const signer = createSigner(keyPair); const nextKeySigner = createSigner(currNextKey); const logEvent = await logManager.createLogEvent({ nextKeyHashes: [nextKeyHash], - signer, nextKeySigner, }); From 57b44bc709cfffea78ee59618a4bb6809888018a Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 16 Apr 2025 22:36:30 +0530 Subject: [PATCH 2/8] fix: w3id builder --- infrastructure/w3id/src/index.ts | 19 +++++- infrastructure/w3id/src/logs/log-manager.ts | 3 +- infrastructure/w3id/tests/logs/log.test.ts | 6 +- infrastructure/w3id/tests/w3id.test.ts | 74 +++++++++++++++++++++ 4 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 infrastructure/w3id/tests/w3id.test.ts diff --git a/infrastructure/w3id/src/index.ts b/infrastructure/w3id/src/index.ts index 1ccfcc39..340e9057 100644 --- a/infrastructure/w3id/src/index.ts +++ b/infrastructure/w3id/src/index.ts @@ -11,7 +11,7 @@ import { generateRandomAlphaNum } from "./utils/rand"; import { v4 as uuidv4 } from "uuid"; import { generateUuid } from "./utils/uuid"; -class W3ID { +export class W3ID { constructor( public id: string, public logs?: IDLogManager, @@ -23,6 +23,7 @@ export class W3IDBuilder { private repository?: StorageSpec; private entropy?: string; private namespace?: string; + private nextKeyHash?: string; public withEntropy(str: string): W3IDBuilder { this.entropy = str; @@ -46,7 +47,12 @@ export class W3IDBuilder { return this; } - public build(): W3ID { + public withNextKeyHash(hash: string): W3IDBuilder { + this.nextKeyHash = hash; + return this; + } + + public async build(): Promise { this.entropy = this.entropy ?? generateRandomAlphaNum(); this.namespace = this.namespace ?? uuidv4(); const id = generateUuid(this.entropy, this.namespace); @@ -57,7 +63,16 @@ export class W3IDBuilder { throw new Error( "Repository is required, pass with \`withRepository\` method", ); + + if (!this.nextKeyHash) + throw new Error( + "NextKeyHash is required pass with \`withNextKeyHash\` method", + ); const logs = new IDLogManager(this.repository, this.signer); + await logs.createLogEvent({ + id, + nextKeyHashes: [this.nextKeyHash], + }); return new W3ID(id, logs); } } diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index 0b024c03..32d3d62e 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -132,9 +132,10 @@ export class IDLogManager { private async createGenesisEntry(options: GenesisLogOptions) { const { id, nextKeyHashes } = options; + const idTag = id.includes("@") ? id.split("@")[1] : id; const logEvent: LogEvent = { id, - versionId: `0-${id.split("@")[1]}`, + versionId: `0-${idTag}`, versionTime: new Date(Date.now()), updateKeys: [this.signer.pubKey], nextKeyHashes: nextKeyHashes, diff --git a/infrastructure/w3id/tests/logs/log.test.ts b/infrastructure/w3id/tests/logs/log.test.ts index 57576732..1bf2f789 100644 --- a/infrastructure/w3id/tests/logs/log.test.ts +++ b/infrastructure/w3id/tests/logs/log.test.ts @@ -24,7 +24,7 @@ import { MalformedIndexChainError, } from "../../src/errors/errors.ts"; -class InMemoryStorage +export class InMemoryStorage implements StorageSpec { private data: K[] = []; @@ -69,7 +69,7 @@ const signer = createSigner(keyPair); const logManager = new IDLogManager(InMemoryStorage.build(), signer); const w3id = `@${generateUuid("asdfa")}`; -const verifierCallback: VerifierCallback = async ( +export const verifierCallback: VerifierCallback = async ( message: string, signature: string, pubKey: string, @@ -86,7 +86,7 @@ const verifierCallback: VerifierCallback = async ( return isValid; }; -function createSigner(keyPair: nacl.SignKeyPair): Signer { +export function createSigner(keyPair: nacl.SignKeyPair): Signer { const publicKey = uint8ArrayToHex(keyPair.publicKey); const signer: Signer = { pubKey: publicKey, diff --git a/infrastructure/w3id/tests/w3id.test.ts b/infrastructure/w3id/tests/w3id.test.ts new file mode 100644 index 00000000..6ad133c9 --- /dev/null +++ b/infrastructure/w3id/tests/w3id.test.ts @@ -0,0 +1,74 @@ +import { W3ID, W3IDBuilder } from "../src"; +import { describe, test, expect } from "vitest"; +import falso from "@ngneat/falso"; +import nacl from "tweetnacl"; +import { + createSigner, + InMemoryStorage, + verifierCallback, +} from "./logs/log.test"; +import { IDLogManager } from "../src/logs/log-manager"; +import { hash } from "../src/utils/hash"; +import { uint8ArrayToHex } from "../src/utils/codec"; +import { LogEvent } from "../src/logs/log.types"; + +const keyPair = nacl.sign.keyPair(); + +describe("W3IDBuilder", () => { + test("ID Generation: Create Basic ID", async () => { + const id = await new W3IDBuilder().build(); + expect(id).toBeInstanceOf(W3ID); + expect(id.logs).toBeUndefined(); + }); + + test("ID Generation: UUID is Deterministic", async () => { + const namespace = falso.randUuid(); + const entropy = falso.randText(); + + const id1 = await new W3IDBuilder() + .withEntropy(entropy) + .withNamespace(namespace) + .build(); + + const id2 = await new W3IDBuilder() + .withEntropy(entropy) + .withNamespace(namespace) + .build(); + expect(id1.id).toEqual(id2.id); + expect(id1.logs).toBeUndefined(); + }); + + test("ID Generation: Creates IDLogManager", async () => { + const id = await new W3IDBuilder() + .withRepository(InMemoryStorage.build()) + .withSigner(createSigner(keyPair)) + .withNextKeyHash(falso.randText()) + .build(); + expect(id.logs).toBeInstanceOf(IDLogManager); + const genesisLog = (await id.logs?.repository.findMany({}))[0]; + expect(genesisLog).toBeDefined(); + }); + + test("ID Mutation: Key Rotation Works", async () => { + const nextKeyPair = nacl.sign.keyPair(); + const nextKeyHash = await hash(uint8ArrayToHex(nextKeyPair.publicKey)); + + const id = await new W3IDBuilder() + .withRepository(InMemoryStorage.build()) + .withSigner(createSigner(keyPair)) + .withNextKeyHash(nextKeyHash) + .build(); + + await id.logs?.createLogEvent({ + nextKeySigner: createSigner(nextKeyPair), + nextKeyHashes: [falso.randText()], + }); + + const logs = await id.logs?.repository.findMany({}); + const result = await IDLogManager.validateLogChain( + logs as LogEvent[], + verifierCallback, + ); + expect(result).toBe(true); + }); +}); From 454b7ea8ec9a71bffefce344903b0aaeb7bea129 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 16 Apr 2025 22:45:29 +0530 Subject: [PATCH 3/8] feat: add global config var for w3id --- infrastructure/w3id/src/index.ts | 10 ++- infrastructure/w3id/tests/logs/log.test.ts | 83 ++-------------------- infrastructure/w3id/tests/utils/crypto.ts | 38 ++++++++++ infrastructure/w3id/tests/utils/store.ts | 40 +++++++++++ infrastructure/w3id/tests/w3id.test.ts | 17 +++-- 5 files changed, 103 insertions(+), 85 deletions(-) create mode 100644 infrastructure/w3id/tests/utils/crypto.ts create mode 100644 infrastructure/w3id/tests/utils/store.ts diff --git a/infrastructure/w3id/src/index.ts b/infrastructure/w3id/src/index.ts index 340e9057..efa4efe3 100644 --- a/infrastructure/w3id/src/index.ts +++ b/infrastructure/w3id/src/index.ts @@ -24,12 +24,18 @@ export class W3IDBuilder { private entropy?: string; private namespace?: string; private nextKeyHash?: string; + private global?: boolean = false; public withEntropy(str: string): W3IDBuilder { this.entropy = str; return this; } + public withGlobal(isGlobal: boolean): W3IDBuilder { + this.global = isGlobal; + return this; + } + public withNamespace(uuid: string): W3IDBuilder { this.namespace = uuid; return this; @@ -55,7 +61,9 @@ export class W3IDBuilder { public async build(): Promise { this.entropy = this.entropy ?? generateRandomAlphaNum(); this.namespace = this.namespace ?? uuidv4(); - const id = generateUuid(this.entropy, this.namespace); + const id = this.global + ? "@" + : "" + generateUuid(this.entropy, this.namespace); if (!this.signer) { return new W3ID(id); } else { diff --git a/infrastructure/w3id/tests/logs/log.test.ts b/infrastructure/w3id/tests/logs/log.test.ts index 1bf2f789..ad3e8701 100644 --- a/infrastructure/w3id/tests/logs/log.test.ts +++ b/infrastructure/w3id/tests/logs/log.test.ts @@ -1,20 +1,10 @@ -import { StorageSpec } from "../../src/logs/storage/storage-spec.ts"; -import { - LogEvent, - Signer, - VerifierCallback, -} from "../../src/logs/log.types.ts"; +import { LogEvent } from "../../src/logs/log.types.ts"; import { IDLogManager } from "../../src/logs/log-manager"; import { generateUuid } from "../../src/utils/uuid"; import { describe, expect, test, expectTypeOf } from "vitest"; import { hash } from "../../src/utils/hash"; import nacl from "tweetnacl"; -import { - uint8ArrayToHex, - stringToUint8Array, - hexToUint8Array, -} from "../../src/utils/codec"; -import { base58btc } from "multiformats/bases/base58"; +import { uint8ArrayToHex } from "../../src/utils/codec"; import falso from "@ngneat/falso"; import { BadNextKeySpecifiedError, @@ -23,44 +13,9 @@ import { MalformedHashChainError, MalformedIndexChainError, } from "../../src/errors/errors.ts"; +import { InMemoryStorage } from "../utils/store.ts"; +import { createSigner, verifierCallback } from "../utils/crypto.ts"; -export class InMemoryStorage - implements StorageSpec -{ - private data: K[] = []; - - public static build(): StorageSpec< - T, - K - > { - return new InMemoryStorage(); - } - - public async create(body: T): Promise { - const entry = body as unknown as K; - this.data.push(entry); - return entry; - } - - public async findOne(options: Partial): Promise { - const result = this.data.find((item) => - Object.entries(options).every( - ([key, value]) => item[key as keyof K] === value, - ), - ); - - if (!result) throw new Error("Not found"); - return result; - } - - public async findMany(options: Partial): Promise { - return this.data.filter((item) => - Object.entries(options).every( - ([key, value]) => item[key as keyof K] === value, - ), - ); - } -} const keyPair = nacl.sign.keyPair(); let currNextKey = nacl.sign.keyPair(); @@ -69,36 +24,6 @@ const signer = createSigner(keyPair); const logManager = new IDLogManager(InMemoryStorage.build(), signer); const w3id = `@${generateUuid("asdfa")}`; -export const verifierCallback: VerifierCallback = async ( - message: string, - signature: string, - pubKey: string, -) => { - const signatureBuffer = base58btc.decode(signature); - const messageBuffer = stringToUint8Array(message); - const publicKey = hexToUint8Array(pubKey); - const isValid = nacl.sign.detached.verify( - messageBuffer, - signatureBuffer, - publicKey, - ); - - return isValid; -}; - -export function createSigner(keyPair: nacl.SignKeyPair): Signer { - const publicKey = uint8ArrayToHex(keyPair.publicKey); - const signer: Signer = { - pubKey: publicKey, - sign: (str: string) => { - const buffer = stringToUint8Array(str); - const signature = nacl.sign.detached(buffer, keyPair.secretKey); - return base58btc.encode(signature); - }, - }; - return signer; -} - describe("LogManager", async () => { test("GenesisEvent: [Throw at Bad Options]", async () => { const nextKeyHash = await hash(uint8ArrayToHex(currNextKey.publicKey)); diff --git a/infrastructure/w3id/tests/utils/crypto.ts b/infrastructure/w3id/tests/utils/crypto.ts new file mode 100644 index 00000000..6c9de1fb --- /dev/null +++ b/infrastructure/w3id/tests/utils/crypto.ts @@ -0,0 +1,38 @@ +import { base58btc } from "multiformats/bases/base58"; +import { Signer, VerifierCallback } from "../../src/logs/log.types"; +import { + hexToUint8Array, + stringToUint8Array, + uint8ArrayToHex, +} from "../../src/utils/codec"; +import nacl from "tweetnacl"; + +export const verifierCallback: VerifierCallback = async ( + message: string, + signature: string, + pubKey: string, +) => { + const signatureBuffer = base58btc.decode(signature); + const messageBuffer = stringToUint8Array(message); + const publicKey = hexToUint8Array(pubKey); + const isValid = nacl.sign.detached.verify( + messageBuffer, + signatureBuffer, + publicKey, + ); + + return isValid; +}; + +export function createSigner(keyPair: nacl.SignKeyPair): Signer { + const publicKey = uint8ArrayToHex(keyPair.publicKey); + const signer: Signer = { + pubKey: publicKey, + sign: (str: string) => { + const buffer = stringToUint8Array(str); + const signature = nacl.sign.detached(buffer, keyPair.secretKey); + return base58btc.encode(signature); + }, + }; + return signer; +} diff --git a/infrastructure/w3id/tests/utils/store.ts b/infrastructure/w3id/tests/utils/store.ts new file mode 100644 index 00000000..f13541f4 --- /dev/null +++ b/infrastructure/w3id/tests/utils/store.ts @@ -0,0 +1,40 @@ +import { LogEvent } from "../../src/logs/log.types"; +import { StorageSpec } from "../../src/logs/storage/storage-spec.ts"; + +export class InMemoryStorage + implements StorageSpec +{ + private data: K[] = []; + + public static build(): StorageSpec< + T, + K + > { + return new InMemoryStorage(); + } + + public async create(body: T): Promise { + const entry = body as unknown as K; + this.data.push(entry); + return entry; + } + + public async findOne(options: Partial): Promise { + const result = this.data.find((item) => + Object.entries(options).every( + ([key, value]) => item[key as keyof K] === value, + ), + ); + + if (!result) throw new Error("Not found"); + return result; + } + + public async findMany(options: Partial): Promise { + return this.data.filter((item) => + Object.entries(options).every( + ([key, value]) => item[key as keyof K] === value, + ), + ); + } +} diff --git a/infrastructure/w3id/tests/w3id.test.ts b/infrastructure/w3id/tests/w3id.test.ts index 6ad133c9..9266674c 100644 --- a/infrastructure/w3id/tests/w3id.test.ts +++ b/infrastructure/w3id/tests/w3id.test.ts @@ -2,11 +2,8 @@ import { W3ID, W3IDBuilder } from "../src"; import { describe, test, expect } from "vitest"; import falso from "@ngneat/falso"; import nacl from "tweetnacl"; -import { - createSigner, - InMemoryStorage, - verifierCallback, -} from "./logs/log.test"; +import { createSigner, verifierCallback } from "./utils/crypto"; +import { InMemoryStorage } from "./utils/store"; import { IDLogManager } from "../src/logs/log-manager"; import { hash } from "../src/utils/hash"; import { uint8ArrayToHex } from "../src/utils/codec"; @@ -21,6 +18,16 @@ describe("W3IDBuilder", () => { expect(id.logs).toBeUndefined(); }); + test("ID Generation: Global ID begins with `@`", async () => { + const id = await new W3IDBuilder().withGlobal(true).build(); + expect(id.id.startsWith("@")).toBe(true); + }); + + test("ID Generation: Local ID begins doesn't begin with `@`", async () => { + const id = await new W3IDBuilder().build(); + expect(id.id.startsWith("@")).toBe(false); + }); + test("ID Generation: UUID is Deterministic", async () => { const namespace = falso.randUuid(); const entropy = falso.randText(); From 9e0580a743aa8db3448c4d330cf6a91e40e64066 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 16 Apr 2025 22:59:32 +0530 Subject: [PATCH 4/8] chore: add docs --- infrastructure/w3id/src/errors/errors.ts | 10 +- infrastructure/w3id/src/index.ts | 172 ++++++----- infrastructure/w3id/src/logs/log-manager.ts | 312 +++++++++++--------- infrastructure/w3id/src/logs/log.types.ts | 40 +-- infrastructure/w3id/src/utils/rand.ts | 18 +- 5 files changed, 307 insertions(+), 245 deletions(-) diff --git a/infrastructure/w3id/src/errors/errors.ts b/infrastructure/w3id/src/errors/errors.ts index 4a314b08..31bc90ef 100644 --- a/infrastructure/w3id/src/errors/errors.ts +++ b/infrastructure/w3id/src/errors/errors.ts @@ -1,33 +1,33 @@ export class MalformedIndexChainError extends Error { - constructor(message: string = "Malformed index chain detected") { + constructor(message = "Malformed index chain detected") { super(message); this.name = "MalformedIndexChainError"; } } export class MalformedHashChainError extends Error { - constructor(message: string = "Malformed hash chain detected") { + constructor(message = "Malformed hash chain detected") { super(message); this.name = "MalformedHashChainError"; } } export class BadSignatureError extends Error { - constructor(message: string = "Bad signature detected") { + constructor(message = "Bad signature detected") { super(message); this.name = "BadSignatureError"; } } export class BadNextKeySpecifiedError extends Error { - constructor(message: string = "Bad next key specified") { + constructor(message = "Bad next key specified") { super(message); this.name = "BadNextKeySpecifiedError"; } } export class BadOptionsSpecifiedError extends Error { - constructor(message: string = "Bad options specified") { + constructor(message = "Bad options specified") { super(message); this.name = "BadOptionsSpecifiedError"; } diff --git a/infrastructure/w3id/src/index.ts b/infrastructure/w3id/src/index.ts index efa4efe3..5e7cb997 100644 --- a/infrastructure/w3id/src/index.ts +++ b/infrastructure/w3id/src/index.ts @@ -1,87 +1,121 @@ -/** - * According to the project specification there are supposed to be 2 main types of - * W3ID's ones which are tied to more tangible entities and hence need rotation - * and others which are just UUIDs. Hence the approach to do this in a builder pattern - */ - import { IDLogManager } from "./logs/log-manager"; -import { LogEvent, Signer } from "./logs/log.types"; -import { StorageSpec } from "./logs/storage/storage-spec"; +import type { LogEvent, Signer } from "./logs/log.types"; +import type { StorageSpec } from "./logs/storage/storage-spec"; import { generateRandomAlphaNum } from "./utils/rand"; import { v4 as uuidv4 } from "uuid"; import { generateUuid } from "./utils/uuid"; export class W3ID { - constructor( - public id: string, - public logs?: IDLogManager, - ) {} + constructor( + public id: string, + public logs?: IDLogManager, + ) {} } export class W3IDBuilder { - private signer?: Signer; - private repository?: StorageSpec; - private entropy?: string; - private namespace?: string; - private nextKeyHash?: string; - private global?: boolean = false; + private signer?: Signer; + private repository?: StorageSpec; + private entropy?: string; + private namespace?: string; + private nextKeyHash?: string; + private global?: boolean = false; - public withEntropy(str: string): W3IDBuilder { - this.entropy = str; - return this; - } + /** + * Specify entropy to create the identity with + * + * @param {string} str + */ + public withEntropy(str: string): W3IDBuilder { + this.entropy = str; + return this; + } - public withGlobal(isGlobal: boolean): W3IDBuilder { - this.global = isGlobal; - return this; - } + /** + * Specify namespace to use to generate the UUIDv5 + * + * @param {string} uuid + */ + public withNamespace(uuid: string): W3IDBuilder { + this.namespace = uuid; + return this; + } - public withNamespace(uuid: string): W3IDBuilder { - this.namespace = uuid; - return this; - } + /** + * Specify whether to create a global identifier or a local identifer + * + * According to the project specification there are supposed to be 2 main types of + * W3ID's ones which are tied to more permanent entities + * + * A global identifer is expected to live at the registry and starts with an \`@\` + * + * @param {boolean} isGlobal + */ + public withGlobal(isGlobal: boolean): W3IDBuilder { + this.global = isGlobal; + return this; + } - public withRepository( - storage: StorageSpec, - ): W3IDBuilder { - this.repository = storage; - return this; - } + /** + * Add a logs repository to the W3ID, a rotateble key attached W3ID would need a + * repository in which the logs would be stored + * + * @param {StorageSpec} storage + */ + public withRepository(storage: StorageSpec): W3IDBuilder { + this.repository = storage; + return this; + } - public withSigner(signer: Signer): W3IDBuilder { - this.signer = signer; - return this; - } + /** + * Attach a keypair to the W3ID, a key attached W3ID would also need a repository + * to be added. + * + * @param {Signer} signer + */ + public withSigner(signer: Signer): W3IDBuilder { + this.signer = signer; + return this; + } - public withNextKeyHash(hash: string): W3IDBuilder { - this.nextKeyHash = hash; - return this; - } + /** + * Specify the SHA256 hash of the next key which will sign the next log entry after + * rotation of keys + * + * @param {string} hash + */ + public withNextKeyHash(hash: string): W3IDBuilder { + this.nextKeyHash = hash; + return this; + } - public async build(): Promise { - this.entropy = this.entropy ?? generateRandomAlphaNum(); - this.namespace = this.namespace ?? uuidv4(); - const id = this.global - ? "@" - : "" + generateUuid(this.entropy, this.namespace); - if (!this.signer) { - return new W3ID(id); - } else { - if (!this.repository) - throw new Error( - "Repository is required, pass with \`withRepository\` method", - ); + /** + * Build the W3ID with provided builder options + * + * @returns Promise + */ + public async build(): Promise { + this.entropy = this.entropy ?? generateRandomAlphaNum(); + this.namespace = this.namespace ?? uuidv4(); + const id = `${ + this.global ? "@" : "" + }${generateUuid(this.entropy, this.namespace)}`; + if (!this.signer) { + return new W3ID(id); + } + if (!this.repository) + throw new Error( + "Repository is required, pass with `withRepository` method", + ); - if (!this.nextKeyHash) - throw new Error( - "NextKeyHash is required pass with \`withNextKeyHash\` method", - ); - const logs = new IDLogManager(this.repository, this.signer); - await logs.createLogEvent({ - id, - nextKeyHashes: [this.nextKeyHash], - }); - return new W3ID(id, logs); - } - } + if (!this.nextKeyHash) + throw new Error( + "NextKeyHash is required pass with `withNextKeyHash` method", + ); + const logs = new IDLogManager(this.repository, this.signer); + await logs.createLogEvent({ + id, + nextKeyHashes: [this.nextKeyHash], + }); + return new W3ID(id, logs); + } } diff --git a/infrastructure/w3id/src/logs/log-manager.ts b/infrastructure/w3id/src/logs/log-manager.ts index 32d3d62e..97a7d90f 100644 --- a/infrastructure/w3id/src/logs/log-manager.ts +++ b/infrastructure/w3id/src/logs/log-manager.ts @@ -1,22 +1,22 @@ import canonicalize from "canonicalize"; import { - BadNextKeySpecifiedError, - BadOptionsSpecifiedError, - BadSignatureError, - MalformedHashChainError, - MalformedIndexChainError, + BadNextKeySpecifiedError, + BadOptionsSpecifiedError, + BadSignatureError, + MalformedHashChainError, + MalformedIndexChainError, } from "../errors/errors"; import { isSubsetOf } from "../utils/array"; import { hash } from "../utils/hash"; import { - isGenesisOptions, - isRotationOptions, - type Signer, - type CreateLogEventOptions, - type GenesisLogOptions, - type LogEvent, - type RotationLogOptions, - type VerifierCallback, + isGenesisOptions, + isRotationOptions, + type Signer, + type CreateLogEventOptions, + type GenesisLogOptions, + type LogEvent, + type RotationLogOptions, + type VerifierCallback, } from "./log.types"; import type { StorageSpec } from "./storage/storage-spec"; @@ -28,133 +28,161 @@ import type { StorageSpec } from "./storage/storage-spec"; // TODO: Create a specification link inside our docs for how generation of identifier works export class IDLogManager { - repository: StorageSpec; - signer: Signer; - - constructor(repository: StorageSpec, signer: Signer) { - this.repository = repository; - this.signer = signer; - } - - static async validateLogChain( - log: LogEvent[], - verifyCallback: VerifierCallback, - ) { - let currIndex = 0; - let currentNextKeyHashesSeen: string[] = []; - let lastUpdateKeysSeen: string[] = []; - let lastHash: string | null = null; - - for (const e of log) { - const [_index, _hash] = e.versionId.split("-"); - const index = Number(_index); - if (currIndex !== index) throw new MalformedIndexChainError(); - const hashedUpdateKeys = await Promise.all( - e.updateKeys.map(async (k) => await hash(k)), - ); - if (index > 0) { - const updateKeysSeen = isSubsetOf( - hashedUpdateKeys, - currentNextKeyHashesSeen, - ); - if (!updateKeysSeen || lastHash !== _hash) - throw new MalformedHashChainError(); - } - - currentNextKeyHashesSeen = e.nextKeyHashes; - await IDLogManager.verifyLogEventProof( - e, - lastUpdateKeysSeen.length > 0 - ? lastUpdateKeysSeen - : e.updateKeys, - verifyCallback, - ); - lastUpdateKeysSeen = e.updateKeys; - currIndex++; - lastHash = await hash(canonicalize(e) as string); - } - return true; - } - - private static async verifyLogEventProof( - e: LogEvent, - currentUpdateKeys: string[], - verifyCallback: VerifierCallback, - ) { - const proof = e.proof; - const copy = JSON.parse(JSON.stringify(e)); - // biome-ignore lint/performance/noDelete: we need to delete proof completely - delete copy.proof; - const canonicalJson = canonicalize(copy); - let verified = false; - if (!proof) - throw new BadSignatureError("No proof found in the log event."); - for (const key of currentUpdateKeys) { - const signValidates = await verifyCallback( - canonicalJson as string, - proof, - key, - ); - if (signValidates) verified = true; - } - if (!verified) throw new BadSignatureError(); - } - - private async appendEntry( - entries: LogEvent[], - options: RotationLogOptions, - ) { - const { nextKeyHashes, nextKeySigner } = options; - const latestEntry = entries[entries.length - 1]; - const logHash = await hash(latestEntry); - const index = Number(latestEntry.versionId.split("-")[0]) + 1; - - const currKeyHash = await hash(nextKeySigner.pubKey); - if (!latestEntry.nextKeyHashes.includes(currKeyHash)) - throw new BadNextKeySpecifiedError(); - - const logEvent: LogEvent = { - id: latestEntry.id, - versionTime: new Date(Date.now()), - versionId: `${index}-${logHash}`, - updateKeys: [nextKeySigner.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; - - const proof = await this.signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; - - await this.repository.create(logEvent); - this.signer = nextKeySigner; - return logEvent; - } - - private async createGenesisEntry(options: GenesisLogOptions) { - const { id, nextKeyHashes } = options; - const idTag = id.includes("@") ? id.split("@")[1] : id; - const logEvent: LogEvent = { - id, - versionId: `0-${idTag}`, - versionTime: new Date(Date.now()), - updateKeys: [this.signer.pubKey], - nextKeyHashes: nextKeyHashes, - method: "w3id:v0.0.0", - }; - const proof = await this.signer.sign(canonicalize(logEvent) as string); - logEvent.proof = proof; - await this.repository.create(logEvent); - return logEvent; - } - - async createLogEvent(options: CreateLogEventOptions) { - const entries = await this.repository.findMany({}); - if (entries.length > 0) { - if (!isRotationOptions(options)) - throw new BadOptionsSpecifiedError(); - return this.appendEntry(entries, options); - } - if (!isGenesisOptions(options)) throw new BadOptionsSpecifiedError(); - return this.createGenesisEntry(options); - } + repository: StorageSpec; + signer: Signer; + + constructor(repository: StorageSpec, signer: Signer) { + this.repository = repository; + this.signer = signer; + } + + /** + * Validate a chain of W3ID logs + * + * @param {LogEvent[]} log + * @param {VerifierCallback} verifyCallback + * @returns {Promise} + */ + + static async validateLogChain( + log: LogEvent[], + verifyCallback: VerifierCallback, + ): Promise { + let currIndex = 0; + let currentNextKeyHashesSeen: string[] = []; + let lastUpdateKeysSeen: string[] = []; + let lastHash: string | null = null; + + for (const e of log) { + const [_index, _hash] = e.versionId.split("-"); + const index = Number(_index); + if (currIndex !== index) throw new MalformedIndexChainError(); + const hashedUpdateKeys = await Promise.all( + e.updateKeys.map(async (k) => await hash(k)), + ); + if (index > 0) { + const updateKeysSeen = isSubsetOf( + hashedUpdateKeys, + currentNextKeyHashesSeen, + ); + if (!updateKeysSeen || lastHash !== _hash) + throw new MalformedHashChainError(); + } + + currentNextKeyHashesSeen = e.nextKeyHashes; + await IDLogManager.verifyLogEventProof( + e, + lastUpdateKeysSeen.length > 0 ? lastUpdateKeysSeen : e.updateKeys, + verifyCallback, + ); + lastUpdateKeysSeen = e.updateKeys; + currIndex++; + lastHash = await hash(canonicalize(e) as string); + } + return true; + } + + /** + * Validate cryptographic signature on a single LogEvent + * + * @param {LogEvent} e + * @param {string[]} currentUpdateKeys + * @param {VerifierCallback} verifyCallback + * @returns {Promise} + */ + private static async verifyLogEventProof( + e: LogEvent, + currentUpdateKeys: string[], + verifyCallback: VerifierCallback, + ): Promise { + const proof = e.proof; + const copy = JSON.parse(JSON.stringify(e)); + // biome-ignore lint/performance/noDelete: we need to delete proof completely + delete copy.proof; + const canonicalJson = canonicalize(copy); + let verified = false; + if (!proof) throw new BadSignatureError("No proof found in the log event."); + for (const key of currentUpdateKeys) { + const signValidates = await verifyCallback( + canonicalJson as string, + proof, + key, + ); + if (signValidates) verified = true; + } + if (!verified) throw new BadSignatureError(); + } + + /** + * Append a new log entry for a W3ID + * + * @param {LogEvent[]} entries + * @param {RotationLogOptions} options + * @returns Promise + */ + private async appendEntry(entries: LogEvent[], options: RotationLogOptions) { + const { nextKeyHashes, nextKeySigner } = options; + const latestEntry = entries[entries.length - 1]; + const logHash = await hash(latestEntry); + const index = Number(latestEntry.versionId.split("-")[0]) + 1; + + const currKeyHash = await hash(nextKeySigner.pubKey); + if (!latestEntry.nextKeyHashes.includes(currKeyHash)) + throw new BadNextKeySpecifiedError(); + + const logEvent: LogEvent = { + id: latestEntry.id, + versionTime: new Date(Date.now()), + versionId: `${index}-${logHash}`, + updateKeys: [nextKeySigner.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; + + const proof = await this.signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; + + await this.repository.create(logEvent); + this.signer = nextKeySigner; + return logEvent; + } + + /** + * Create genesis entry for a W3ID log + * + * @param {GenesisLogOptions} options + * @returns Promise + */ + private async createGenesisEntry(options: GenesisLogOptions) { + const { id, nextKeyHashes } = options; + const idTag = id.includes("@") ? id.split("@")[1] : id; + const logEvent: LogEvent = { + id, + versionId: `0-${idTag}`, + versionTime: new Date(Date.now()), + updateKeys: [this.signer.pubKey], + nextKeyHashes: nextKeyHashes, + method: "w3id:v0.0.0", + }; + const proof = await this.signer.sign(canonicalize(logEvent) as string); + logEvent.proof = proof; + await this.repository.create(logEvent); + return logEvent; + } + + /** + * Create a log event and save it to the repository + * + * @param {CreateLogEventOptions} options + * @returns Promise + */ + async createLogEvent(options: CreateLogEventOptions): Promise { + const entries = await this.repository.findMany({}); + if (entries.length > 0) { + if (!isRotationOptions(options)) throw new BadOptionsSpecifiedError(); + return this.appendEntry(entries, options); + } + if (!isGenesisOptions(options)) throw new BadOptionsSpecifiedError(); + return this.createGenesisEntry(options); + } } diff --git a/infrastructure/w3id/src/logs/log.types.ts b/infrastructure/w3id/src/logs/log.types.ts index 39fa47af..a5c1636d 100644 --- a/infrastructure/w3id/src/logs/log.types.ts +++ b/infrastructure/w3id/src/logs/log.types.ts @@ -1,43 +1,43 @@ export type LogEvent = { - id: string; - versionId: string; - versionTime: Date; - updateKeys: string[]; - nextKeyHashes: string[]; - method: `w3id:v${string}`; - proof?: string; + id: string; + versionId: string; + versionTime: Date; + updateKeys: string[]; + nextKeyHashes: string[]; + method: `w3id:v${string}`; + proof?: string; }; export type VerifierCallback = ( - message: string, - signature: string, - pubKey: string, + message: string, + signature: string, + pubKey: string, ) => Promise; export type Signer = { - sign: (message: string) => Promise | string; - pubKey: string; + sign: (message: string) => Promise | string; + pubKey: string; }; export type RotationLogOptions = { - nextKeyHashes: string[]; - nextKeySigner: Signer; + nextKeyHashes: string[]; + nextKeySigner: Signer; }; export type GenesisLogOptions = { - nextKeyHashes: string[]; - id: string; + nextKeyHashes: string[]; + id: string; }; export function isGenesisOptions( - options: CreateLogEventOptions, + options: CreateLogEventOptions, ): options is GenesisLogOptions { - return "id" in options; + return "id" in options; } export function isRotationOptions( - options: CreateLogEventOptions, + options: CreateLogEventOptions, ): options is RotationLogOptions { - return "nextKeySigner" in options; + return "nextKeySigner" in options; } export type CreateLogEventOptions = GenesisLogOptions | RotationLogOptions; diff --git a/infrastructure/w3id/src/utils/rand.ts b/infrastructure/w3id/src/utils/rand.ts index bad43ea7..c78206dc 100644 --- a/infrastructure/w3id/src/utils/rand.ts +++ b/infrastructure/w3id/src/utils/rand.ts @@ -5,15 +5,15 @@ * @returns {string} */ -export function generateRandomAlphaNum(length: number = 16): string { - const chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - let result = ""; - const charsLength = chars.length; +export function generateRandomAlphaNum(length = 16): string { + const chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + const charsLength = chars.length; - for (let i = 0; i < length; i++) { - result += chars.charAt(Math.floor(Math.random() * charsLength)); - } + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * charsLength)); + } - return result; + return result; } From d2fca54b69c4d60b3a4678e9e1291f3788d6d03e Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 16 Apr 2025 23:26:31 +0530 Subject: [PATCH 5/8] chore: change rand to crng --- infrastructure/w3id/src/utils/rand.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/infrastructure/w3id/src/utils/rand.ts b/infrastructure/w3id/src/utils/rand.ts index c78206dc..f9710fe0 100644 --- a/infrastructure/w3id/src/utils/rand.ts +++ b/infrastructure/w3id/src/utils/rand.ts @@ -10,9 +10,12 @@ export function generateRandomAlphaNum(length = 16): string { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; const charsLength = chars.length; + const randomValues = new Uint32Array(length); + + crypto.getRandomValues(randomValues); for (let i = 0; i < length; i++) { - result += chars.charAt(Math.floor(Math.random() * charsLength)); + result += chars.charAt(randomValues[i] % charsLength); } return result; From 9e2f093da28f34a6d0f35a8116eb19053fc31c8c Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 16 Apr 2025 23:30:13 +0530 Subject: [PATCH 6/8] chore: add ts type again --- infrastructure/w3id/src/utils/rand.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/infrastructure/w3id/src/utils/rand.ts b/infrastructure/w3id/src/utils/rand.ts index f9710fe0..7eb084f8 100644 --- a/infrastructure/w3id/src/utils/rand.ts +++ b/infrastructure/w3id/src/utils/rand.ts @@ -5,18 +5,18 @@ * @returns {string} */ -export function generateRandomAlphaNum(length = 16): string { - const chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - let result = ""; - const charsLength = chars.length; - const randomValues = new Uint32Array(length); +export function generateRandomAlphaNum(length: number = 16): string { + const chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + const charsLength = chars.length; + const randomValues = new Uint32Array(length); - crypto.getRandomValues(randomValues); + crypto.getRandomValues(randomValues); - for (let i = 0; i < length; i++) { - result += chars.charAt(randomValues[i] % charsLength); - } + for (let i = 0; i < length; i++) { + result += chars.charAt(randomValues[i] % charsLength); + } - return result; + return result; } From 9791ea6e8d7e075727503db944e5e8d0e2be6520 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 16 Apr 2025 23:31:26 +0530 Subject: [PATCH 7/8] chore: fix lint and format --- infrastructure/w3id/src/utils/rand.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/infrastructure/w3id/src/utils/rand.ts b/infrastructure/w3id/src/utils/rand.ts index 7eb084f8..f9710fe0 100644 --- a/infrastructure/w3id/src/utils/rand.ts +++ b/infrastructure/w3id/src/utils/rand.ts @@ -5,18 +5,18 @@ * @returns {string} */ -export function generateRandomAlphaNum(length: number = 16): string { - const chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - let result = ""; - const charsLength = chars.length; - const randomValues = new Uint32Array(length); +export function generateRandomAlphaNum(length = 16): string { + const chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + const charsLength = chars.length; + const randomValues = new Uint32Array(length); - crypto.getRandomValues(randomValues); + crypto.getRandomValues(randomValues); - for (let i = 0; i < length; i++) { - result += chars.charAt(randomValues[i] % charsLength); - } + for (let i = 0; i < length; i++) { + result += chars.charAt(randomValues[i] % charsLength); + } - return result; + return result; } From e8ed604d7a4c3f60237e370c6e15220b4b7168c1 Mon Sep 17 00:00:00 2001 From: Merul Dhiman Date: Wed, 16 Apr 2025 23:37:35 +0530 Subject: [PATCH 8/8] chore: add w3id tests github workflow --- .github/workflows/tests-w3id.yml | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/tests-w3id.yml diff --git a/.github/workflows/tests-w3id.yml b/.github/workflows/tests-w3id.yml new file mode 100644 index 00000000..8c60b42e --- /dev/null +++ b/.github/workflows/tests-w3id.yml @@ -0,0 +1,34 @@ +name: Tests [W3ID] + +on: + push: + branches: [main] + paths: + - 'infrastructure/w3id/**' + pull_request: + branches: [main] + paths: + - 'infrastructure/w3id/**' + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install pnpm + run: npm install -g pnpm + + - name: Install dependencies + run: pnpm install + + - name: Run tests + run: pnpm -F=w3id test +