Skip to content

Commit dcd1b00

Browse files
committed
feat: create w3id builder
1 parent 6bccab0 commit dcd1b00

File tree

5 files changed

+237
-153
lines changed

5 files changed

+237
-153
lines changed

infrastructure/w3id/src/index.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,64 @@
1-
export default {};
1+
/**
2+
* According to the project specification there are supposed to be 2 main types of
3+
* W3ID's ones which are tied to more tangible entities and hence need rotation
4+
* and others which are just UUIDs. Hence the approach to do this in a builder pattern
5+
*/
6+
7+
import { IDLogManager } from "./logs/log-manager";
8+
import { LogEvent, Signer } from "./logs/log.types";
9+
import { StorageSpec } from "./logs/storage/storage-spec";
10+
import { generateRandomAlphaNum } from "./utils/rand";
11+
import { v4 as uuidv4 } from "uuid";
12+
import { generateUuid } from "./utils/uuid";
13+
14+
class W3ID {
15+
constructor(
16+
public id: string,
17+
public logs?: IDLogManager,
18+
) {}
19+
}
20+
21+
export class W3IDBuilder {
22+
private signer?: Signer;
23+
private repository?: StorageSpec<LogEvent, LogEvent>;
24+
private entropy?: string;
25+
private namespace?: string;
26+
27+
public withEntropy(str: string): W3IDBuilder {
28+
this.entropy = str;
29+
return this;
30+
}
31+
32+
public withNamespace(uuid: string): W3IDBuilder {
33+
this.namespace = uuid;
34+
return this;
35+
}
36+
37+
public withRepository(
38+
storage: StorageSpec<LogEvent, LogEvent>,
39+
): W3IDBuilder {
40+
this.repository = storage;
41+
return this;
42+
}
43+
44+
public withSigner(signer: Signer): W3IDBuilder {
45+
this.signer = signer;
46+
return this;
47+
}
48+
49+
public build(): W3ID {
50+
this.entropy = this.entropy ?? generateRandomAlphaNum();
51+
this.namespace = this.namespace ?? uuidv4();
52+
const id = generateUuid(this.entropy, this.namespace);
53+
if (!this.signer) {
54+
return new W3ID(id);
55+
} else {
56+
if (!this.repository)
57+
throw new Error(
58+
"Repository is required, pass with \`withRepository\` method",
59+
);
60+
const logs = new IDLogManager(this.repository, this.signer);
61+
return new W3ID(id, logs);
62+
}
63+
}
64+
}
Lines changed: 129 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
import canonicalize from "canonicalize";
22
import {
3-
BadNextKeySpecifiedError,
4-
BadOptionsSpecifiedError,
5-
BadSignatureError,
6-
MalformedHashChainError,
7-
MalformedIndexChainError,
3+
BadNextKeySpecifiedError,
4+
BadOptionsSpecifiedError,
5+
BadSignatureError,
6+
MalformedHashChainError,
7+
MalformedIndexChainError,
88
} from "../errors/errors";
99
import { isSubsetOf } from "../utils/array";
1010
import { hash } from "../utils/hash";
1111
import {
12-
isGenesisOptions,
13-
isRotationOptions,
14-
type CreateLogEventOptions,
15-
type GenesisLogOptions,
16-
type LogEvent,
17-
type RotationLogOptions,
18-
type VerifierCallback,
12+
isGenesisOptions,
13+
isRotationOptions,
14+
type Signer,
15+
type CreateLogEventOptions,
16+
type GenesisLogOptions,
17+
type LogEvent,
18+
type RotationLogOptions,
19+
type VerifierCallback,
1920
} from "./log.types";
2021
import type { StorageSpec } from "./storage/storage-spec";
2122

@@ -27,122 +28,132 @@ import type { StorageSpec } from "./storage/storage-spec";
2728
// TODO: Create a specification link inside our docs for how generation of identifier works
2829

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

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

36-
static async validateLogChain(
37-
log: LogEvent[],
38-
verifyCallback: VerifierCallback,
39-
) {
40-
let currIndex = 0;
41-
let currentNextKeyHashesSeen: string[] = [];
42-
let lastUpdateKeysSeen: string[] = [];
43-
let lastHash: string | null = null;
39+
static async validateLogChain(
40+
log: LogEvent[],
41+
verifyCallback: VerifierCallback,
42+
) {
43+
let currIndex = 0;
44+
let currentNextKeyHashesSeen: string[] = [];
45+
let lastUpdateKeysSeen: string[] = [];
46+
let lastHash: string | null = null;
4447

45-
for (const e of log) {
46-
const [_index, _hash] = e.versionId.split("-");
47-
const index = Number(_index);
48-
if (currIndex !== index) throw new MalformedIndexChainError();
49-
const hashedUpdateKeys = await Promise.all(
50-
e.updateKeys.map(async (k) => await hash(k)),
51-
);
52-
if (index > 0) {
53-
const updateKeysSeen = isSubsetOf(
54-
hashedUpdateKeys,
55-
currentNextKeyHashesSeen,
56-
);
57-
if (!updateKeysSeen || lastHash !== _hash)
58-
throw new MalformedHashChainError();
59-
}
48+
for (const e of log) {
49+
const [_index, _hash] = e.versionId.split("-");
50+
const index = Number(_index);
51+
if (currIndex !== index) throw new MalformedIndexChainError();
52+
const hashedUpdateKeys = await Promise.all(
53+
e.updateKeys.map(async (k) => await hash(k)),
54+
);
55+
if (index > 0) {
56+
const updateKeysSeen = isSubsetOf(
57+
hashedUpdateKeys,
58+
currentNextKeyHashesSeen,
59+
);
60+
if (!updateKeysSeen || lastHash !== _hash)
61+
throw new MalformedHashChainError();
62+
}
6063

61-
currentNextKeyHashesSeen = e.nextKeyHashes;
62-
await IDLogManager.verifyLogEventProof(
63-
e,
64-
lastUpdateKeysSeen.length > 0 ? lastUpdateKeysSeen : e.updateKeys,
65-
verifyCallback,
66-
);
67-
lastUpdateKeysSeen = e.updateKeys;
68-
currIndex++;
69-
lastHash = await hash(canonicalize(e) as string);
70-
}
71-
return true;
72-
}
64+
currentNextKeyHashesSeen = e.nextKeyHashes;
65+
await IDLogManager.verifyLogEventProof(
66+
e,
67+
lastUpdateKeysSeen.length > 0
68+
? lastUpdateKeysSeen
69+
: e.updateKeys,
70+
verifyCallback,
71+
);
72+
lastUpdateKeysSeen = e.updateKeys;
73+
currIndex++;
74+
lastHash = await hash(canonicalize(e) as string);
75+
}
76+
return true;
77+
}
7378

74-
private static async verifyLogEventProof(
75-
e: LogEvent,
76-
currentUpdateKeys: string[],
77-
verifyCallback: VerifierCallback,
78-
) {
79-
const proof = e.proof;
80-
const copy = JSON.parse(JSON.stringify(e));
81-
// biome-ignore lint/performance/noDelete: we need to delete proof completely
82-
delete copy.proof;
83-
const canonicalJson = canonicalize(copy);
84-
let verified = false;
85-
if (!proof) throw new BadSignatureError("No proof found in the log event.");
86-
for (const key of currentUpdateKeys) {
87-
const signValidates = await verifyCallback(
88-
canonicalJson as string,
89-
proof,
90-
key,
91-
);
92-
if (signValidates) verified = true;
93-
}
94-
if (!verified) throw new BadSignatureError();
95-
}
79+
private static async verifyLogEventProof(
80+
e: LogEvent,
81+
currentUpdateKeys: string[],
82+
verifyCallback: VerifierCallback,
83+
) {
84+
const proof = e.proof;
85+
const copy = JSON.parse(JSON.stringify(e));
86+
// biome-ignore lint/performance/noDelete: we need to delete proof completely
87+
delete copy.proof;
88+
const canonicalJson = canonicalize(copy);
89+
let verified = false;
90+
if (!proof)
91+
throw new BadSignatureError("No proof found in the log event.");
92+
for (const key of currentUpdateKeys) {
93+
const signValidates = await verifyCallback(
94+
canonicalJson as string,
95+
proof,
96+
key,
97+
);
98+
if (signValidates) verified = true;
99+
}
100+
if (!verified) throw new BadSignatureError();
101+
}
96102

97-
private async appendEntry(entries: LogEvent[], options: RotationLogOptions) {
98-
const { signer, nextKeyHashes, nextKeySigner } = options;
99-
const latestEntry = entries[entries.length - 1];
100-
const logHash = await hash(latestEntry);
101-
const index = Number(latestEntry.versionId.split("-")[0]) + 1;
103+
private async appendEntry(
104+
entries: LogEvent[],
105+
options: RotationLogOptions,
106+
) {
107+
const { nextKeyHashes, nextKeySigner } = options;
108+
const latestEntry = entries[entries.length - 1];
109+
const logHash = await hash(latestEntry);
110+
const index = Number(latestEntry.versionId.split("-")[0]) + 1;
102111

103-
const currKeyHash = await hash(nextKeySigner.pubKey);
104-
if (!latestEntry.nextKeyHashes.includes(currKeyHash))
105-
throw new BadNextKeySpecifiedError();
112+
const currKeyHash = await hash(nextKeySigner.pubKey);
113+
if (!latestEntry.nextKeyHashes.includes(currKeyHash))
114+
throw new BadNextKeySpecifiedError();
106115

107-
const logEvent: LogEvent = {
108-
id: latestEntry.id,
109-
versionTime: new Date(Date.now()),
110-
versionId: `${index}-${logHash}`,
111-
updateKeys: [nextKeySigner.pubKey],
112-
nextKeyHashes: nextKeyHashes,
113-
method: "w3id:v0.0.0",
114-
};
116+
const logEvent: LogEvent = {
117+
id: latestEntry.id,
118+
versionTime: new Date(Date.now()),
119+
versionId: `${index}-${logHash}`,
120+
updateKeys: [nextKeySigner.pubKey],
121+
nextKeyHashes: nextKeyHashes,
122+
method: "w3id:v0.0.0",
123+
};
115124

116-
const proof = await signer.sign(canonicalize(logEvent) as string);
117-
logEvent.proof = proof;
125+
const proof = await this.signer.sign(canonicalize(logEvent) as string);
126+
logEvent.proof = proof;
118127

119-
await this.repository.create(logEvent);
120-
return logEvent;
121-
}
128+
await this.repository.create(logEvent);
129+
this.signer = nextKeySigner;
130+
return logEvent;
131+
}
122132

123-
private async createGenesisEntry(options: GenesisLogOptions) {
124-
const { id, nextKeyHashes, signer } = options;
125-
const logEvent: LogEvent = {
126-
id,
127-
versionId: `0-${id.split("@")[1]}`,
128-
versionTime: new Date(Date.now()),
129-
updateKeys: [signer.pubKey],
130-
nextKeyHashes: nextKeyHashes,
131-
method: "w3id:v0.0.0",
132-
};
133-
const proof = await signer.sign(canonicalize(logEvent) as string);
134-
logEvent.proof = proof;
135-
await this.repository.create(logEvent);
136-
return logEvent;
137-
}
133+
private async createGenesisEntry(options: GenesisLogOptions) {
134+
const { id, nextKeyHashes } = options;
135+
const logEvent: LogEvent = {
136+
id,
137+
versionId: `0-${id.split("@")[1]}`,
138+
versionTime: new Date(Date.now()),
139+
updateKeys: [this.signer.pubKey],
140+
nextKeyHashes: nextKeyHashes,
141+
method: "w3id:v0.0.0",
142+
};
143+
const proof = await this.signer.sign(canonicalize(logEvent) as string);
144+
logEvent.proof = proof;
145+
await this.repository.create(logEvent);
146+
return logEvent;
147+
}
138148

139-
async createLogEvent(options: CreateLogEventOptions) {
140-
const entries = await this.repository.findMany({});
141-
if (entries.length > 0) {
142-
if (!isRotationOptions(options)) throw new BadOptionsSpecifiedError();
143-
return this.appendEntry(entries, options);
144-
}
145-
if (!isGenesisOptions(options)) throw new BadOptionsSpecifiedError();
146-
return this.createGenesisEntry(options);
147-
}
149+
async createLogEvent(options: CreateLogEventOptions) {
150+
const entries = await this.repository.findMany({});
151+
if (entries.length > 0) {
152+
if (!isRotationOptions(options))
153+
throw new BadOptionsSpecifiedError();
154+
return this.appendEntry(entries, options);
155+
}
156+
if (!isGenesisOptions(options)) throw new BadOptionsSpecifiedError();
157+
return this.createGenesisEntry(options);
158+
}
148159
}

0 commit comments

Comments
 (0)