Skip to content

Commit 078e5e3

Browse files
authored
Feat/core id creation logic (#99)
* feat: create w3id builder * fix: w3id builder * feat: add global config var for w3id * chore: add docs * chore: change rand to crng * chore: add ts type again * chore: fix lint and format * chore: add w3id tests github workflow
1 parent 6bccab0 commit 078e5e3

File tree

10 files changed

+399
-108
lines changed

10 files changed

+399
-108
lines changed

.github/workflows/tests-w3id.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Tests [W3ID]
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'infrastructure/w3id/**'
8+
pull_request:
9+
branches: [main]
10+
paths:
11+
- 'infrastructure/w3id/**'
12+
13+
jobs:
14+
test:
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Checkout code
19+
uses: actions/checkout@v3
20+
21+
- name: Set up Node.js 22
22+
uses: actions/setup-node@v4
23+
with:
24+
node-version: 22
25+
26+
- name: Install pnpm
27+
run: npm install -g pnpm
28+
29+
- name: Install dependencies
30+
run: pnpm install
31+
32+
- name: Run tests
33+
run: pnpm -F=w3id test
34+

infrastructure/w3id/src/errors/errors.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
export class MalformedIndexChainError extends Error {
2-
constructor(message: string = "Malformed index chain detected") {
2+
constructor(message = "Malformed index chain detected") {
33
super(message);
44
this.name = "MalformedIndexChainError";
55
}
66
}
77

88
export class MalformedHashChainError extends Error {
9-
constructor(message: string = "Malformed hash chain detected") {
9+
constructor(message = "Malformed hash chain detected") {
1010
super(message);
1111
this.name = "MalformedHashChainError";
1212
}
1313
}
1414

1515
export class BadSignatureError extends Error {
16-
constructor(message: string = "Bad signature detected") {
16+
constructor(message = "Bad signature detected") {
1717
super(message);
1818
this.name = "BadSignatureError";
1919
}
2020
}
2121

2222
export class BadNextKeySpecifiedError extends Error {
23-
constructor(message: string = "Bad next key specified") {
23+
constructor(message = "Bad next key specified") {
2424
super(message);
2525
this.name = "BadNextKeySpecifiedError";
2626
}
2727
}
2828

2929
export class BadOptionsSpecifiedError extends Error {
30-
constructor(message: string = "Bad options specified") {
30+
constructor(message = "Bad options specified") {
3131
super(message);
3232
this.name = "BadOptionsSpecifiedError";
3333
}

infrastructure/w3id/src/index.ts

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,121 @@
1-
export default {};
1+
import { IDLogManager } from "./logs/log-manager";
2+
import type { LogEvent, Signer } from "./logs/log.types";
3+
import type { StorageSpec } from "./logs/storage/storage-spec";
4+
import { generateRandomAlphaNum } from "./utils/rand";
5+
import { v4 as uuidv4 } from "uuid";
6+
import { generateUuid } from "./utils/uuid";
7+
8+
export class W3ID {
9+
constructor(
10+
public id: string,
11+
public logs?: IDLogManager,
12+
) {}
13+
}
14+
15+
export class W3IDBuilder {
16+
private signer?: Signer;
17+
private repository?: StorageSpec<LogEvent, LogEvent>;
18+
private entropy?: string;
19+
private namespace?: string;
20+
private nextKeyHash?: string;
21+
private global?: boolean = false;
22+
23+
/**
24+
* Specify entropy to create the identity with
25+
*
26+
* @param {string} str
27+
*/
28+
public withEntropy(str: string): W3IDBuilder {
29+
this.entropy = str;
30+
return this;
31+
}
32+
33+
/**
34+
* Specify namespace to use to generate the UUIDv5
35+
*
36+
* @param {string} uuid
37+
*/
38+
public withNamespace(uuid: string): W3IDBuilder {
39+
this.namespace = uuid;
40+
return this;
41+
}
42+
43+
/**
44+
* Specify whether to create a global identifier or a local identifer
45+
*
46+
* According to the project specification there are supposed to be 2 main types of
47+
* W3ID's ones which are tied to more permanent entities
48+
*
49+
* A global identifer is expected to live at the registry and starts with an \`@\`
50+
*
51+
* @param {boolean} isGlobal
52+
*/
53+
public withGlobal(isGlobal: boolean): W3IDBuilder {
54+
this.global = isGlobal;
55+
return this;
56+
}
57+
58+
/**
59+
* Add a logs repository to the W3ID, a rotateble key attached W3ID would need a
60+
* repository in which the logs would be stored
61+
*
62+
* @param {StorageSpec<LogEvent, LogEvent>} storage
63+
*/
64+
public withRepository(storage: StorageSpec<LogEvent, LogEvent>): W3IDBuilder {
65+
this.repository = storage;
66+
return this;
67+
}
68+
69+
/**
70+
* Attach a keypair to the W3ID, a key attached W3ID would also need a repository
71+
* to be added.
72+
*
73+
* @param {Signer} signer
74+
*/
75+
public withSigner(signer: Signer): W3IDBuilder {
76+
this.signer = signer;
77+
return this;
78+
}
79+
80+
/**
81+
* Specify the SHA256 hash of the next key which will sign the next log entry after
82+
* rotation of keys
83+
*
84+
* @param {string} hash
85+
*/
86+
public withNextKeyHash(hash: string): W3IDBuilder {
87+
this.nextKeyHash = hash;
88+
return this;
89+
}
90+
91+
/**
92+
* Build the W3ID with provided builder options
93+
*
94+
* @returns Promise<W3ID>
95+
*/
96+
public async build(): Promise<W3ID> {
97+
this.entropy = this.entropy ?? generateRandomAlphaNum();
98+
this.namespace = this.namespace ?? uuidv4();
99+
const id = `${
100+
this.global ? "@" : ""
101+
}${generateUuid(this.entropy, this.namespace)}`;
102+
if (!this.signer) {
103+
return new W3ID(id);
104+
}
105+
if (!this.repository)
106+
throw new Error(
107+
"Repository is required, pass with `withRepository` method",
108+
);
109+
110+
if (!this.nextKeyHash)
111+
throw new Error(
112+
"NextKeyHash is required pass with `withNextKeyHash` method",
113+
);
114+
const logs = new IDLogManager(this.repository, this.signer);
115+
await logs.createLogEvent({
116+
id,
117+
nextKeyHashes: [this.nextKeyHash],
118+
});
119+
return new W3ID(id, logs);
120+
}
121+
}

infrastructure/w3id/src/logs/log-manager.ts

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { hash } from "../utils/hash";
1111
import {
1212
isGenesisOptions,
1313
isRotationOptions,
14+
type Signer,
1415
type CreateLogEventOptions,
1516
type GenesisLogOptions,
1617
type LogEvent,
@@ -28,15 +29,25 @@ import type { StorageSpec } from "./storage/storage-spec";
2829

2930
export class IDLogManager {
3031
repository: StorageSpec<LogEvent, LogEvent>;
32+
signer: Signer;
3133

32-
constructor(repository: StorageSpec<LogEvent, LogEvent>) {
34+
constructor(repository: StorageSpec<LogEvent, LogEvent>, signer: Signer) {
3335
this.repository = repository;
36+
this.signer = signer;
3437
}
3538

39+
/**
40+
* Validate a chain of W3ID logs
41+
*
42+
* @param {LogEvent[]} log
43+
* @param {VerifierCallback} verifyCallback
44+
* @returns {Promise<true>}
45+
*/
46+
3647
static async validateLogChain(
3748
log: LogEvent[],
3849
verifyCallback: VerifierCallback,
39-
) {
50+
): Promise<true> {
4051
let currIndex = 0;
4152
let currentNextKeyHashesSeen: string[] = [];
4253
let lastUpdateKeysSeen: string[] = [];
@@ -71,11 +82,19 @@ export class IDLogManager {
7182
return true;
7283
}
7384

85+
/**
86+
* Validate cryptographic signature on a single LogEvent
87+
*
88+
* @param {LogEvent} e
89+
* @param {string[]} currentUpdateKeys
90+
* @param {VerifierCallback} verifyCallback
91+
* @returns {Promise<void>}
92+
*/
7493
private static async verifyLogEventProof(
7594
e: LogEvent,
7695
currentUpdateKeys: string[],
7796
verifyCallback: VerifierCallback,
78-
) {
97+
): Promise<void> {
7998
const proof = e.proof;
8099
const copy = JSON.parse(JSON.stringify(e));
81100
// biome-ignore lint/performance/noDelete: we need to delete proof completely
@@ -94,8 +113,15 @@ export class IDLogManager {
94113
if (!verified) throw new BadSignatureError();
95114
}
96115

116+
/**
117+
* Append a new log entry for a W3ID
118+
*
119+
* @param {LogEvent[]} entries
120+
* @param {RotationLogOptions} options
121+
* @returns Promise<LogEvent>
122+
*/
97123
private async appendEntry(entries: LogEvent[], options: RotationLogOptions) {
98-
const { signer, nextKeyHashes, nextKeySigner } = options;
124+
const { nextKeyHashes, nextKeySigner } = options;
99125
const latestEntry = entries[entries.length - 1];
100126
const logHash = await hash(latestEntry);
101127
const index = Number(latestEntry.versionId.split("-")[0]) + 1;
@@ -113,30 +139,44 @@ export class IDLogManager {
113139
method: "w3id:v0.0.0",
114140
};
115141

116-
const proof = await signer.sign(canonicalize(logEvent) as string);
142+
const proof = await this.signer.sign(canonicalize(logEvent) as string);
117143
logEvent.proof = proof;
118144

119145
await this.repository.create(logEvent);
146+
this.signer = nextKeySigner;
120147
return logEvent;
121148
}
122149

150+
/**
151+
* Create genesis entry for a W3ID log
152+
*
153+
* @param {GenesisLogOptions} options
154+
* @returns Promise<LogEvent>
155+
*/
123156
private async createGenesisEntry(options: GenesisLogOptions) {
124-
const { id, nextKeyHashes, signer } = options;
157+
const { id, nextKeyHashes } = options;
158+
const idTag = id.includes("@") ? id.split("@")[1] : id;
125159
const logEvent: LogEvent = {
126160
id,
127-
versionId: `0-${id.split("@")[1]}`,
161+
versionId: `0-${idTag}`,
128162
versionTime: new Date(Date.now()),
129-
updateKeys: [signer.pubKey],
163+
updateKeys: [this.signer.pubKey],
130164
nextKeyHashes: nextKeyHashes,
131165
method: "w3id:v0.0.0",
132166
};
133-
const proof = await signer.sign(canonicalize(logEvent) as string);
167+
const proof = await this.signer.sign(canonicalize(logEvent) as string);
134168
logEvent.proof = proof;
135169
await this.repository.create(logEvent);
136170
return logEvent;
137171
}
138172

139-
async createLogEvent(options: CreateLogEventOptions) {
173+
/**
174+
* Create a log event and save it to the repository
175+
*
176+
* @param {CreateLogEventOptions} options
177+
* @returns Promise<LogEvent>
178+
*/
179+
async createLogEvent(options: CreateLogEventOptions): Promise<LogEvent> {
140180
const entries = await this.repository.findMany({});
141181
if (entries.length > 0) {
142182
if (!isRotationOptions(options)) throw new BadOptionsSpecifiedError();

infrastructure/w3id/src/logs/log.types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,12 @@ export type Signer = {
2121

2222
export type RotationLogOptions = {
2323
nextKeyHashes: string[];
24-
signer: Signer;
2524
nextKeySigner: Signer;
2625
};
2726

2827
export type GenesisLogOptions = {
2928
nextKeyHashes: string[];
3029
id: string;
31-
signer: Signer;
3230
};
3331

3432
export function isGenesisOptions(
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Generate a random alphanumeric sequence with set length
3+
*
4+
* @param {number} length length of the alphanumeric string you want
5+
* @returns {string}
6+
*/
7+
8+
export function generateRandomAlphaNum(length = 16): string {
9+
const chars =
10+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
11+
let result = "";
12+
const charsLength = chars.length;
13+
const randomValues = new Uint32Array(length);
14+
15+
crypto.getRandomValues(randomValues);
16+
17+
for (let i = 0; i < length; i++) {
18+
result += chars.charAt(randomValues[i] % charsLength);
19+
}
20+
21+
return result;
22+
}

0 commit comments

Comments
 (0)