diff --git a/.zed/tasks.json b/.zed/tasks.json index 78bdfaf..d8ce466 100644 --- a/.zed/tasks.json +++ b/.zed/tasks.json @@ -5,5 +5,9 @@ { "label": "Deno Test", "command": "deno test -A" + }, + { + "label": "Deno Test - Watch", + "command": "deno test -A --watch" } ] diff --git a/identity/identity.ts b/identity/identity.ts index b212bb5..68c5fbe 100644 --- a/identity/identity.ts +++ b/identity/identity.ts @@ -3,8 +3,9 @@ import { BlahPublicKey, type BlahSignedPayload, } from "../crypto/mod.ts"; -import type { BlahActKeyRecord } from "./actKey.ts"; +import { type BlahActKeyRecord, blahActKeyRecordSchema } from "./actKey.ts"; import { blahIdentityFileSchema } from "./identityFile.ts"; +import { BlahIdentityFile } from "./mod.ts"; import type { BlahProfile } from "./profile.ts"; type InternalActKey = { @@ -21,6 +22,8 @@ type ActKey = { comment: string; }; +type ActKeyConfig = Partial>; + async function constructActKeyFromRaw( raw: BlahSignedPayload, idKey: BlahPublicKey | BlahKeyPair, @@ -39,6 +42,39 @@ async function constructActKeyFromRaw( return { raw, key, expiresAt, sigValid }; } +async function constructInternalActKey( + idKeyPair: BlahKeyPair, + key: BlahPublicKey | BlahKeyPair, + config?: ActKeyConfig, +): Promise { + const actKey: ActKey = { + publicKey: key instanceof BlahKeyPair ? key.publicKey : key, + expiresAt: new Date(Date.now() + 365 * 24 * 3600 * 1000), + sigValid: true, + comment: "", + ...config, + }; + + const rawRecord = await idKeyPair + .signPayload(blahActKeyRecordSchema.parse( + { + typ: "user_act_key", + expire_time: Math.floor(actKey.expiresAt.getTime() / 1000), + comment: actKey.comment, + act_key: actKey.publicKey.id, + } satisfies BlahActKeyRecord, + )); + + const internalActKey: InternalActKey = { + raw: rawRecord, + key, + expiresAt: actKey.expiresAt, + sigValid: true, + }; + + return internalActKey; +} + export class BlahIdentity { private internalIdKey: BlahPublicKey | BlahKeyPair; private internalActKeys: InternalActKey[]; @@ -57,6 +93,10 @@ export class BlahIdentity { this.internalProfileSigValid = internalProfileSigValid; } + get profileSigValid(): boolean { + return this.internalProfileSigValid; + } + get profile(): BlahProfile { return this.rawProfile.signee.payload; } @@ -124,30 +164,13 @@ export class BlahIdentity { idKeyPair: BlahKeyPair, firstActKey: BlahKeyPair, profile: BlahProfile, - firstActKeyConfig: Partial>, + firstActKeyConfig?: ActKeyConfig, ): Promise { - const actKey: ActKey = { - publicKey: firstActKey.publicKey, - expiresAt: new Date(Date.now() + 365 * 24 * 3600 * 1000), - sigValid: true, - comment: "", - ...firstActKeyConfig, - }; - - const actKeyRecord: BlahSignedPayload = await idKeyPair - .signPayload({ - typ: "user_act_key", - expire_time: actKey.expiresAt.getTime() / 1000, - comment: actKey.comment, - act_key: actKey.publicKey.id, - }); - - const internalActKey: InternalActKey = { - raw: actKeyRecord, - key: firstActKey, - expiresAt: actKey.expiresAt, - sigValid: true, - }; + const internalActKey = await constructInternalActKey( + idKeyPair, + firstActKey, + firstActKeyConfig, + ); const profileRecord: BlahSignedPayload = await firstActKey .signPayload(profile); @@ -159,4 +182,50 @@ export class BlahIdentity { true, ); } + + generateIdentityFile(): BlahIdentityFile { + return blahIdentityFileSchema.parse( + { + id_key: this.idPublicKey.id, + act_keys: this.internalActKeys.map((k) => (k.raw)), + profile: this.rawProfile, + } satisfies BlahIdentityFile, + ); + } + + async addActKey(actKey: BlahKeyPair | BlahPublicKey, config?: ActKeyConfig) { + if (this.internalIdKey instanceof BlahPublicKey) { + throw new Error("Cannot add act key to identity without ID key pair."); + } + + const internalActKey = await constructInternalActKey( + this.internalIdKey, + actKey, + config, + ); + + this.internalActKeys.push(internalActKey); + } + + async updateActKey( + keyId: string, + config: ActKeyConfig, + ) { + if (this.internalIdKey instanceof BlahPublicKey) { + throw new Error("Cannot update act key in identity without ID key pair."); + } + + const actKeyIndex = this.internalActKeys.findIndex( + (k) => k.key.id === keyId, + ); + if (actKeyIndex === -1) { + throw new Error("Act key not found in identity."); + } + + this.internalActKeys[actKeyIndex] = await constructInternalActKey( + this.internalIdKey, + this.internalActKeys[actKeyIndex].key, + config, + ); + } } diff --git a/identity/identityFile.ts b/identity/identityFile.ts index fb7355e..b78010e 100644 --- a/identity/identityFile.ts +++ b/identity/identityFile.ts @@ -6,7 +6,7 @@ import type { BlahSignedPayload } from "../crypto/mod.ts"; export const blahIdentityFileSchema = z.object({ id_key: z.string(), - act_keys: z.array(blahSignedPayloadSchemaOf(blahActKeyRecordSchema)), + act_keys: z.array(blahSignedPayloadSchemaOf(blahActKeyRecordSchema)).min(1), profile: blahSignedPayloadSchemaOf(blahProfileSchema), });