diff --git a/.changeset/smart-buckets-end.md b/.changeset/smart-buckets-end.md new file mode 100644 index 0000000..57d3f5d --- /dev/null +++ b/.changeset/smart-buckets-end.md @@ -0,0 +1,9 @@ +--- +"agentcommercekit": minor +"@agentcommercekit/keys": minor +"@agentcommercekit/did": minor +--- + +- Upgrade legacy public key formats to use multibase in DID Documents +- Update base64 methods to be explicit that they use `base64url` encoding +- Simplify interface for public key encoding methods diff --git a/demos/payments/src/receipt-service.ts b/demos/payments/src/receipt-service.ts index 80a55b2..76c1d53 100644 --- a/demos/payments/src/receipt-service.ts +++ b/demos/payments/src/receipt-service.ts @@ -1,6 +1,12 @@ import { serve } from "@hono/node-server" import { logger } from "@repo/api-utils/middleware/logger" -import { colors, errorMessage, log, successMessage } from "@repo/cli-tools" +import { + colors, + errorMessage, + log, + logJson, + successMessage +} from "@repo/cli-tools" import { createPaymentReceipt, getDidResolver, @@ -77,10 +83,8 @@ app.post("/", async (c) => { const paymentDetails = v.parse(paymentDetailsSchema, parsed.payload) - log( - colors.dim("Payment details:"), - colors.cyan(JSON.stringify(paymentDetails, null, 2)) - ) + log(colors.dim("Payment details:")) + logJson(paymentDetails, colors.cyan) log(colors.dim("Verifying payment token...")) // Verify the payment token is not expired, etc. diff --git a/examples/issuer/README.md b/examples/issuer/README.md index 3d17b08..f757ae1 100644 --- a/examples/issuer/README.md +++ b/examples/issuer/README.md @@ -4,8 +4,8 @@ This example showcases a **Credential Issuer** for [ACK-ID](https://www.agentcom The API allows for the issuance, verification, and revocation of the following credential types: -- `ControllerCredential`: ACK-ID credentials that prove DID ownership heirarchies. -- `PaymentReceiptCredential`: ACK-Pay creddentials that provide proof of payment that satisfies a given Payment Request. +- `ControllerCredential`: ACK-ID credentials that prove DID ownership hierarchies. +- `PaymentReceiptCredential`: ACK-Pay credentials that provide proof of payment that satisfies a given Payment Request. This issuer supports credential revocation using [StatusList2021](https://www.w3.org/community/reports/credentials/CG-FINAL-vc-status-list-2021-20230102/), which is a privacy-preserving, space-efficient mechanism for maintaining a credential revocation list. diff --git a/examples/issuer/src/test-helpers/did-web-with-signer.ts b/examples/issuer/src/test-helpers/did-web-with-signer.ts index 1e9c278..fc44d55 100644 --- a/examples/issuer/src/test-helpers/did-web-with-signer.ts +++ b/examples/issuer/src/test-helpers/did-web-with-signer.ts @@ -27,7 +27,7 @@ export async function createDidWebWithSigner( keypair, baseUrl, controller, - format: "jwk" + encoding: "jwk" }) const signer = createJwtSigner(keypair) diff --git a/examples/local-did-host/README.md b/examples/local-did-host/README.md index f0cfbe0..8fa1084 100644 --- a/examples/local-did-host/README.md +++ b/examples/local-did-host/README.md @@ -8,7 +8,7 @@ When someone attempts to resolve a `did:web` document, the resolver fetches `.we This example is a basic [Hono](https://hono.dev) server with dynamic `.well-known/did.json` routes, served at subpaths, by default on the host `0.0.0.0:3458`. There are 2 identities, `agent` and `controller`, served from the subpaths `/agent/.well-known/did.json` and `/controller/.well-known/did.json`, respectively. -Per the `did:web` spec, the `did.json` file is a full DID Document. By its nature, the document does not chane often, and should typically be served as a static file from the server. In this example, we are building the DID Document dynamically per-request, which is not suitable for production environments. +Per the `did:web` spec, the `did.json` file is a full DID Document. By its nature, the document does not change often, and should typically be served as a static file from the server. In this example, we are building the DID Document dynamically per-request, which is not suitable for production environments. ## Getting Started diff --git a/examples/verifier/README.md b/examples/verifier/README.md index 90ad423..5a0e151 100644 --- a/examples/verifier/README.md +++ b/examples/verifier/README.md @@ -2,8 +2,8 @@ This example showcases a **Credential Verifier** for [ACK-ID](https://www.agentcommercekit.com/ack-id) and [ACK-Pay](https://www.agentcommercekit.com/ack-pay) Verifiable Credentials. This API is built with [Hono](https://hono.dev). -- `ControllerCredential`: ACK-ID credentials that prove DID ownership heirarchies. -- `PaymentReceiptCredential`: ACK-Pay creddentials that provide proof of payment that satisfies a given Payment Request. +- `ControllerCredential`: ACK-ID credentials that prove DID ownership hierarchies. +- `PaymentReceiptCredential`: ACK-Pay credentials that provide proof of payment that satisfies a given Payment Request. This verifier uses [StatusList2021](https://www.w3.org/community/reports/credentials/CG-FINAL-vc-status-list-2021-20230102/), to check if a credential is revoked.# Installation diff --git a/packages/agentcommercekit/README.md b/packages/agentcommercekit/README.md index 9b144e1..c88d02b 100644 --- a/packages/agentcommercekit/README.md +++ b/packages/agentcommercekit/README.md @@ -8,7 +8,7 @@ To learn more about the Agent Commerce Kit, check out the [documentation](https: ## Installation -We recommend installing the `agentcommercekit` package, which is tree-shakeable and contains everything you need to build for the ACK protiocol, even if you choose to only target ACK-ID or ACK-Pay. This is the simplest way to get started, prevent version conflicts or duplication, and makes it easy to manage updates. +We recommend installing the `agentcommercekit` package, which is tree-shakeable and contains everything you need to build for the ACK protocol, even if you choose to only target ACK-ID or ACK-Pay. This is the simplest way to get started, prevent version conflicts or duplication, and makes it easy to manage updates. ```sh npm i agentcommercekit diff --git a/packages/did/README.md b/packages/did/README.md index e50994d..575ecce 100644 --- a/packages/did/README.md +++ b/packages/did/README.md @@ -85,7 +85,7 @@ const did = createDidKeyUri(keypair) const didDocument = createDidDocumentFromKeypair({ did, keypair, - format: "hex", // Optional, defaults to "jwk" + encoding: "jwk", // Optional, defaults to "jwk" controller: "did:web:controller.example.com" // Optional }) diff --git a/packages/did/src/create-did-document.test.ts b/packages/did/src/create-did-document.test.ts index c0db00c..5697086 100644 --- a/packages/did/src/create-did-document.test.ts +++ b/packages/did/src/create-did-document.test.ts @@ -1,8 +1,8 @@ import { - formatPublicKey, + encodePublicKeyFromKeypair, generateKeypair, keypairAlgorithms, - publicKeyFormats + publicKeyEncodings } from "@agentcommercekit/keys" import { beforeEach, describe, expect, test } from "vitest" import { @@ -10,19 +10,25 @@ import { createDidDocumentFromKeypair, keyConfig } from "./create-did-document" -import type { DidDocument } from "./did-document" -import type { Keypair, PublicKeyFormat } from "@agentcommercekit/keys" +import type { Keypair, PublicKeyEncoding } from "@agentcommercekit/keys" const keyTypeMap = { secp256k1: "EcdsaSecp256k1VerificationKey2019", Ed25519: "Ed25519VerificationKey2018" } as const -const formatToPropertyMap = { - hex: "publicKeyHex", +const encodingMap = { + hex: "multibase", + jwk: "jwk", + multibase: "multibase", + base58: "multibase" +} + +const encodingToPropertyMap = { + hex: "publicKeyMultibase", jwk: "publicKeyJwk", multibase: "publicKeyMultibase", - base58: "publicKeyBase58" + base58: "publicKeyMultibase" } as const describe("createDidDocument() and createDidDocumentFromKeypair()", () => { @@ -41,87 +47,51 @@ describe("createDidDocument() and createDidDocumentFromKeypair()", () => { } as const describe.each(keypairAlgorithms)("algorithm: %s", (algorithm) => { - describe.each(publicKeyFormats)("format: %s", (format: PublicKeyFormat) => { - test(`generates matching documents with ${algorithm} and ${format} format`, () => { - const keypair = keypairMap[algorithm]() - - const documentFromKeypair = createDidDocumentFromKeypair({ - did, - keypair, - format - }) - - let document: DidDocument - switch (format) { - case "hex": - document = createDidDocument({ - did, - publicKey: { - format: "hex", - algorithm, - value: formatPublicKey(keypair, "hex") - } - }) - break - case "jwk": - document = createDidDocument({ - did, - publicKey: { - format: "jwk", - algorithm, - value: formatPublicKey(keypair, "jwk") - } - }) - break - case "multibase": - document = createDidDocument({ - did, - publicKey: { - format: "multibase", - algorithm, - value: formatPublicKey(keypair, "multibase") + describe.each(publicKeyEncodings)( + "encoding: %s", + (encoding: PublicKeyEncoding) => { + test(`generates matching documents with ${algorithm} and ${encoding} encoding`, () => { + const keypair = keypairMap[algorithm]() + + const documentFromKeypair = createDidDocumentFromKeypair({ + did, + keypair, + encoding + }) + + const document = createDidDocument({ + did, + publicKey: encodePublicKeyFromKeypair(encoding, keypair) + }) + + const keyId = `${did}#${encodingMap[encoding]}-1` + const expectedDocument = { + "@context": [ + "https://www.w3.org/ns/did/v1", + ...keyConfig[algorithm].context + ], + id: did, + verificationMethod: [ + { + id: keyId, + type: keyTypeMap[algorithm], + controller: did, + [encodingToPropertyMap[encoding]]: expect.any( + encoding === "jwk" ? Object : String + ) as unknown } - }) - break - case "base58": - document = createDidDocument({ - did, - publicKey: { - format: "base58", - algorithm, - value: formatPublicKey(keypair, "base58") - } - }) - break - } - - const keyId = `${did}#${format}-1` - const expectedDocument = { - "@context": [ - "https://www.w3.org/ns/did/v1", - ...keyConfig[algorithm].context - ], - id: did, - verificationMethod: [ - { - id: keyId, - type: keyTypeMap[algorithm], - controller: did, - [formatToPropertyMap[format]]: expect.any( - format === "jwk" ? Object : String - ) as unknown - } - ], - authentication: [keyId], - assertionMethod: [keyId] - } - - expect(document).toEqual(expectedDocument) - expect(documentFromKeypair).toEqual(expectedDocument) - expect(document.verificationMethod![0]!.id).toBe(keyId) - expect(documentFromKeypair.verificationMethod![0]!.id).toBe(keyId) - }) - }) + ], + authentication: [keyId], + assertionMethod: [keyId] + } + + expect(document).toEqual(expectedDocument) + expect(documentFromKeypair).toEqual(expectedDocument) + expect(document.verificationMethod![0]!.id).toBe(keyId) + expect(documentFromKeypair.verificationMethod![0]!.id).toBe(keyId) + }) + } + ) }) test("includes controller when provided", () => { @@ -135,11 +105,7 @@ describe("createDidDocument() and createDidDocumentFromKeypair()", () => { const document = createDidDocument({ did, - publicKey: { - format: "jwk", - algorithm: "secp256k1", - value: formatPublicKey(secp256k1Keypair, "jwk") - }, + publicKey: encodePublicKeyFromKeypair("jwk", secp256k1Keypair), controller }) @@ -181,19 +147,11 @@ describe("createDidDocument() and createDidDocumentFromKeypair()", () => { const document1 = createDidDocument({ did: did1, - publicKey: { - format: "jwk", - algorithm: "secp256k1", - value: formatPublicKey(secp256k1Keypair, "jwk") - } + publicKey: encodePublicKeyFromKeypair("jwk", secp256k1Keypair) }) const document2 = createDidDocument({ did: did2, - publicKey: { - format: "jwk", - algorithm: "secp256k1", - value: formatPublicKey(secp256k1Keypair, "jwk") - } + publicKey: encodePublicKeyFromKeypair("jwk", secp256k1Keypair) }) expect(document1.verificationMethod![0]!.id).toBe(`${did1}#jwk-1`) @@ -214,11 +172,7 @@ describe("createDidDocument() and createDidDocumentFromKeypair()", () => { const document = createDidDocument({ did, - publicKey: { - format: "jwk", - algorithm: "secp256k1", - value: formatPublicKey(secp256k1Keypair, "jwk") - } + publicKey: encodePublicKeyFromKeypair("jwk", secp256k1Keypair) }) const requiredFields = [ diff --git a/packages/did/src/create-did-document.ts b/packages/did/src/create-did-document.ts index 21d9471..0602582 100644 --- a/packages/did/src/create-did-document.ts +++ b/packages/did/src/create-did-document.ts @@ -1,11 +1,17 @@ -import { formatPublicKey } from "@agentcommercekit/keys" +import { encodePublicKeyFromKeypair } from "@agentcommercekit/keys" +import { + base58ToBytes, + bytesToMultibase, + hexStringToBytes +} from "@agentcommercekit/keys/encoding" import type { DidDocument } from "./did-document" import type { DidUri } from "./did-uri" import type { Keypair, KeypairAlgorithm, - PublicKeyFormat, - PublicKeyTypeMap + PublicKeyEncoding, + PublicKeyTypeMap, + PublicKeyWithEncoding } from "@agentcommercekit/keys" import type { VerificationMethod } from "did-resolver" @@ -23,17 +29,19 @@ export const keyConfig = { } } as const -type PublicKeyWithFormat = { - [K in PublicKeyFormat]: { - format: K +type LegacyPublicKeyEncoding = "hex" | "base58" + +type DidDocumentPublicKey = { + [E in Exclude]: { + encoding: E algorithm: KeypairAlgorithm - value: PublicKeyTypeMap[K] + value: PublicKeyTypeMap[E] } -}[PublicKeyFormat] +}[Exclude] interface CreateVerificationMethodOptions { did: DidUri - publicKey: PublicKeyWithFormat + publicKey: PublicKeyWithEncoding } /** @@ -43,31 +51,49 @@ export function createVerificationMethod({ did, publicKey }: CreateVerificationMethodOptions): VerificationMethod { + const { encoding, algorithm, value } = + convertLegacyPublicKeyToMultibase(publicKey) + const verificationMethod: VerificationMethod = { - id: `${did}#${publicKey.format}-1`, - type: keyConfig[publicKey.algorithm].type, + id: `${did}#${encoding}-1`, + type: keyConfig[algorithm].type, controller: did } // Add public key in the requested format - switch (publicKey.format) { - case "hex": - verificationMethod.publicKeyHex = publicKey.value - break + switch (encoding) { case "jwk": - verificationMethod.publicKeyJwk = publicKey.value + verificationMethod.publicKeyJwk = value break case "multibase": - verificationMethod.publicKeyMultibase = publicKey.value - break - case "base58": - verificationMethod.publicKeyBase58 = publicKey.value + verificationMethod.publicKeyMultibase = value break } return verificationMethod } +function convertLegacyPublicKeyToMultibase( + publicKey: PublicKeyWithEncoding +): DidDocumentPublicKey { + switch (publicKey.encoding) { + case "hex": + return { + encoding: "multibase", + algorithm: publicKey.algorithm, + value: bytesToMultibase(hexStringToBytes(publicKey.value)) + } + case "base58": + return { + encoding: "multibase", + algorithm: publicKey.algorithm, + value: bytesToMultibase(base58ToBytes(publicKey.value)) + } + default: + return publicKey + } +} + /** * Base options for creating a DID document */ @@ -79,7 +105,7 @@ export interface CreateDidDocumentOptions { /** * The public key to include in the DID document */ - publicKey: PublicKeyWithFormat + publicKey: PublicKeyWithEncoding /** * Additional URIs that are equivalent to this DID @@ -161,9 +187,9 @@ export type CreateDidDocumentFromKeypairOptions = Omit< */ keypair: Keypair /** - * The format of the public key + * The encoding of the public key */ - format?: PublicKeyFormat + encoding?: PublicKeyEncoding } /** @@ -174,47 +200,11 @@ export type CreateDidDocumentFromKeypairOptions = Omit< */ export function createDidDocumentFromKeypair({ keypair, - format = "jwk", + encoding = "jwk", ...options }: CreateDidDocumentFromKeypairOptions): DidDocument { - switch (format) { - case "hex": - return createDidDocument({ - ...options, - publicKey: { - format: "hex", - algorithm: keypair.algorithm, - value: formatPublicKey(keypair, "hex") - } - }) - case "jwk": - return createDidDocument({ - ...options, - publicKey: { - format: "jwk", - algorithm: keypair.algorithm, - value: formatPublicKey(keypair, "jwk") - } - }) - case "multibase": - return createDidDocument({ - ...options, - publicKey: { - format: "multibase", - algorithm: keypair.algorithm, - value: formatPublicKey(keypair, "multibase") - } - }) - case "base58": - return createDidDocument({ - ...options, - publicKey: { - format: "base58", - algorithm: keypair.algorithm, - value: formatPublicKey(keypair, "base58") - } - }) - default: - throw new Error(`Invalid format`) - } + return createDidDocument({ + ...options, + publicKey: encodePublicKeyFromKeypair(encoding, keypair) + }) } diff --git a/packages/did/src/did-resolvers/did-resolver.test.ts b/packages/did/src/did-resolvers/did-resolver.test.ts index 513a5d5..abddd9f 100644 --- a/packages/did/src/did-resolvers/did-resolver.test.ts +++ b/packages/did/src/did-resolvers/did-resolver.test.ts @@ -8,7 +8,7 @@ describe("DidResolver", () => { const didDocument = createDidDocument({ did, publicKey: { - format: "hex", + encoding: "hex", algorithm: "secp256k1", value: "0xc0ffee254729296a45a3885639AC7E10F9d54979" } diff --git a/packages/did/src/methods/did-key.test.ts b/packages/did/src/methods/did-key.test.ts index 41c57f0..c279c23 100644 --- a/packages/did/src/methods/did-key.test.ts +++ b/packages/did/src/methods/did-key.test.ts @@ -1,34 +1,38 @@ -import { generateKeypair, keypairFromBase58 } from "@agentcommercekit/keys" +/* eslint-disable @cspell/spellchecker */ +import { generateKeypair, jwkToKeypair } from "@agentcommercekit/keys" import { describe, expect, it } from "vitest" import { createDidKeyUri, isDidKeyUri } from "./did-key" import { getDidResolver } from "../did-resolvers/get-did-resolver" import type { DidUri } from "../did-uri" -import type { KeypairBase58 } from "@agentcommercekit/keys" +import type { PrivateKeyJwk } from "@agentcommercekit/keys/encoding" -const KNOWN_DID_KEYS: { did: DidUri; keypairBase58: KeypairBase58 }[] = [ +const KNOWN_DID_KEYS: { did: DidUri; jwk: PrivateKeyJwk }[] = [ { - did: "did:key:zQ3shuYfyvoY2K83PxQrDr7wS8anFwjVGoKsnEDNjgrn19Udv", - keypairBase58: { - publicKey: "29bWy5f4YzUpWiKAoU34PJzQpHyJJWmqFJHcwPX6svznC", - privateKey: "9Y3KWxCfMccnzGm3ztk8VxgpVpfn5Cf4WJUYtFYs5Fte", - algorithm: "secp256k1" + did: "did:key:zQ3shNCcRrVT3tm43o6JNjSjQaiBXvSb8kHtFhoNGR8eimFZs", + jwk: { + kty: "EC", + crv: "secp256k1", + x: "C70KP9BXCdKBTfjtkQA9xH7uzd7R8hYC6cSpE6CUpro", + y: "2kmbH7YTM-RJ2G596UidxkB3SiG66gi9htsriop766g", + d: "ca66hYvhFSAbm_5YsBTydV2R_hDal-ISv3trPNCFYWg" } }, { - did: "did:key:z6MkqknXmyEp9pQt6cQJLib7KkrSMvFQEKaDJsmw3amDQwzh", - keypairBase58: { - publicKey: "CJXVBizNpGvQz7Zbf9dGUfJSYLyYpSKrcrs1DJoCVjDK", - privateKey: "GyoKth6SiFGEbsSxT7D9dhF9jSCk7W6MLP7Z5815Rkbg", - algorithm: "Ed25519" + did: "did:key:z6MknEES6VA14awWdV27ab5r1jtz3d6ct2wULmvU4YgE1wQ8", + jwk: { + kty: "OKP", + crv: "Ed25519", + x: "c4cfSJSiOFUJffpI06i5Q20X8qc8Vdcw5gCxvcZy9kU", + d: "FIQVa6NvaYXJdRCoI-pNl_ScNKgf_jVjGZf7bHDxEBw" } } ] describe("createDidKeyUri()", () => { it.each(KNOWN_DID_KEYS)( - `generates a valid did:key from a $keypairBase58.algorithm public key`, - async ({ did, keypairBase58 }) => { - const keypair = keypairFromBase58(keypairBase58) + `generates a valid did:key from a $jwk.crv public key`, + async ({ did, jwk }) => { + const keypair = jwkToKeypair(jwk) const didKey = createDidKeyUri(keypair) expect(didKey).toMatch(/^did:key:z[1-9A-HJ-NP-Za-km-z]+$/) diff --git a/packages/did/src/methods/did-pkh.ts b/packages/did/src/methods/did-pkh.ts index a4d498c..a9e0101 100644 --- a/packages/did/src/methods/did-pkh.ts +++ b/packages/did/src/methods/did-pkh.ts @@ -213,7 +213,7 @@ export function createDidPkhDocument({ did, keypair, controller, - format: "hex", + encoding: "hex", additionalContexts, verificationMethod: { id: `${did}#blockchainAccountId`, @@ -230,7 +230,7 @@ export function createDidPkhDocument({ did, keypair, controller, - format: "jwk", + encoding: "jwk", additionalContexts }) diff --git a/packages/did/src/methods/did-web.test.ts b/packages/did/src/methods/did-web.test.ts index c9be8d9..ac13069 100644 --- a/packages/did/src/methods/did-web.test.ts +++ b/packages/did/src/methods/did-web.test.ts @@ -1,5 +1,9 @@ import { generateKeypair } from "@agentcommercekit/keys" -import { bytesToHexString } from "@agentcommercekit/keys/encoding" +import { + bytesToHexString, + bytesToJwk, + bytesToMultibase +} from "@agentcommercekit/keys/encoding" import { describe, expect, it } from "vitest" import { createDidWebDocument, createDidWebUri } from "./did-web" @@ -24,13 +28,48 @@ describe("createDidWeb", () => { }) describe("createDidWebDocument", () => { - it("generates a valid Did and Did document", async () => { + it("generates a valid DidUri and DidDocument with JWK", async () => { + const keypair = await generateKeypair("secp256k1") + const publicKeyJwk = bytesToJwk(keypair.publicKey, keypair.algorithm) + + const { did, didDocument } = createDidWebDocument({ + publicKey: { + encoding: "jwk", + value: publicKeyJwk, + algorithm: keypair.algorithm + }, + baseUrl: "https://example.com" + }) + + expect(did).toEqual("did:web:example.com") + + expect(didDocument).toEqual({ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security#EcdsaSecp256k1VerificationKey2019" + ], + id: "did:web:example.com", + verificationMethod: [ + { + id: "did:web:example.com#jwk-1", + type: "EcdsaSecp256k1VerificationKey2019", + controller: "did:web:example.com", + publicKeyJwk + } + ], + authentication: ["did:web:example.com#jwk-1"], + assertionMethod: ["did:web:example.com#jwk-1"] + }) + }) + + it("generates a valid DidUri and DidDocument, upgrading legacy hex to multibase", async () => { const keypair = await generateKeypair("secp256k1") const publicKeyHex = bytesToHexString(keypair.publicKey) + const publicKeyMultibase = bytesToMultibase(keypair.publicKey) const { did, didDocument } = createDidWebDocument({ publicKey: { - format: "hex", + encoding: "hex", value: publicKeyHex, algorithm: keypair.algorithm }, @@ -47,14 +86,14 @@ describe("createDidWebDocument", () => { id: "did:web:example.com", verificationMethod: [ { - id: "did:web:example.com#hex-1", + id: "did:web:example.com#multibase-1", type: "EcdsaSecp256k1VerificationKey2019", controller: "did:web:example.com", - publicKeyHex: publicKeyHex.replace(/^0x/, "") + publicKeyMultibase } ], - authentication: ["did:web:example.com#hex-1"], - assertionMethod: ["did:web:example.com#hex-1"] + authentication: ["did:web:example.com#multibase-1"], + assertionMethod: ["did:web:example.com#multibase-1"] }) }) }) diff --git a/packages/did/src/resolve-did.test.ts b/packages/did/src/resolve-did.test.ts index 96f395f..bbf971d 100644 --- a/packages/did/src/resolve-did.test.ts +++ b/packages/did/src/resolve-did.test.ts @@ -12,7 +12,7 @@ async function generateDid(baseUrl: string, controller?: DidUri) { return createDidWebDocument({ publicKey: { - format: "hex", + encoding: "hex", value: bytesToHexString(keypair.publicKey), algorithm: keypair.algorithm }, diff --git a/packages/keys/README.md b/packages/keys/README.md index 683b6cd..fe0d2fb 100644 --- a/packages/keys/README.md +++ b/packages/keys/README.md @@ -17,21 +17,19 @@ pnpm add @agentcommercekit/keys ```ts import { generateKeypair, - keypairToBase58, keypairToJwk, - formatPublicKey + encodePublicKeyFromKeypair } from "@agentcommercekit/keys" // Generate and format keypairs const keypair = await generateKeypair("secp256k1") -const base58Keypair = keypairToBase58(keypair) const jwkKeypair = keypairToJwk(keypair) // Format public keys -const hexPublicKey = formatPublicKey(keypair, "hex") -const jwkPublicKey = formatPublicKey(keypair, "jwk") -const multibasePublicKey = formatPublicKey(keypair, "multibase") -const base58PublicKey = formatPublicKey(keypair, "base58") +const hexPublicKey = encodePublicKeyFromKeypair("hex", keypair) +const jwkPublicKey = encodePublicKeyFromKeypair("jwk", keypair) +const multibasePublicKey = encodePublicKeyFromKeypair("multibase", keypair) +const base58PublicKey = encodePublicKeyFromKeypair("base58", keypair) ``` ## API @@ -39,18 +37,12 @@ const base58PublicKey = formatPublicKey(keypair, "base58") ### Keypair Operations - `generateKeypair(algorithm: KeypairAlgorithm, privateKeyBytes?: Uint8Array): Promise` -- `keypairToBase58(keypair: Keypair): KeypairBase58` -- `keypairFromBase58(base58: KeypairBase58): Keypair` - `keypairToJwk(keypair: Keypair): PrivateKeyJwk` - `jwkToKeypair(jwk: PrivateKeyJwk): Keypair` ### Public Key Formatting -- `formatPublicKey(keypair: Keypair, format: T): PublicKeyTypeMap[T]` -- `formatPublicKeyHex(keypair: Keypair): string` -- `formatPublicKeyJwk(keypair: Keypair): PublicKeyJwk` -- `formatPublicKeyMultibase(keypair: Keypair): string` -- `formatPublicKeyBase58(keypair: Keypair): string` +- `encodePublicKeyFromKeypair(encoding: T, keypair: Keypair): PublicKeyTypeMap[T]` - `getCompressedPublicKey(keypair: Keypair): Uint8Array` ### Additional Exports diff --git a/packages/keys/src/encoding/base64.test.ts b/packages/keys/src/encoding/base64.test.ts index 6bbf9f5..3eb75e9 100644 --- a/packages/keys/src/encoding/base64.test.ts +++ b/packages/keys/src/encoding/base64.test.ts @@ -1,36 +1,36 @@ import { describe, expect, test } from "vitest" -import { base64ToBytes, bytesToBase64, isBase64 } from "./base64" +import { base64urlToBytes, bytesToBase64url, isBase64url } from "./base64" describe("base64 encoding and decoding", () => { test("converts bytes to base64 string", () => { const bytes = new Uint8Array([1, 2, 3, 4]) - const base64 = bytesToBase64(bytes) + const base64 = bytesToBase64url(bytes) expect(base64).toBe("AQIDBA") }) test("converts base64 string to bytes", () => { const base64 = "AQIDBA" - const bytes = base64ToBytes(base64) + const bytes = base64urlToBytes(base64) expect(bytes).toEqual(new Uint8Array([1, 2, 3, 4])) }) test("roundtrip base64 encoding", () => { const original = new Uint8Array([1, 2, 3, 4]) - const base64 = bytesToBase64(original) - const bytes = base64ToBytes(base64) + const base64 = bytesToBase64url(original) + const bytes = base64urlToBytes(base64) expect(bytes).toEqual(original) }) }) describe("isBase64", () => { test("returns true for valid base64 strings", () => { - expect(isBase64("AQIDBA")).toBe(true) - expect(isBase64("SGVsbG8sIFdvcmxkIQ")).toBe(true) // "Hello, World!" + expect(isBase64url("AQIDBA")).toBe(true) + expect(isBase64url("SGVsbG8sIFdvcmxkIQ")).toBe(true) // "Hello, World!" }) test("returns false for invalid base64 strings", () => { - expect(isBase64("not base64")).toBe(false) - expect(isBase64("AQIDBA!")).toBe(false) - expect(isBase64(123)).toBe(false) + expect(isBase64url("not base64")).toBe(false) + expect(isBase64url("AQIDBA!")).toBe(false) + expect(isBase64url(123)).toBe(false) }) }) diff --git a/packages/keys/src/encoding/base64.ts b/packages/keys/src/encoding/base64.ts index fe986da..1e4c6e6 100644 --- a/packages/keys/src/encoding/base64.ts +++ b/packages/keys/src/encoding/base64.ts @@ -4,21 +4,21 @@ import { toString } from "uint8arrays/to-string" /** * Convert bytes to a base64url string */ -export function bytesToBase64(bytes: Uint8Array): string { +export function bytesToBase64url(bytes: Uint8Array): string { return toString(bytes, "base64url") } /** * Convert a base64url string to bytes */ -export function base64ToBytes(base64: string): Uint8Array { +export function base64urlToBytes(base64: string): Uint8Array { return fromString(base64, "base64url") } /** * Check if a string is valid base64url encoded */ -export function isBase64(str: unknown): str is string { +export function isBase64url(str: unknown): str is string { if (typeof str !== "string") { return false } diff --git a/packages/keys/src/encoding/hex.test.ts b/packages/keys/src/encoding/hex.test.ts index 8d1b48a..2685a3e 100644 --- a/packages/keys/src/encoding/hex.test.ts +++ b/packages/keys/src/encoding/hex.test.ts @@ -14,6 +14,12 @@ describe("hex encoding and decoding", () => { expect(bytes).toEqual(new Uint8Array([1, 2, 3, 4])) }) + test("handles hex strings in caps", () => { + const hex = "0XABCDEF" + const bytes = hexStringToBytes(hex) + expect(bytes).toEqual(new Uint8Array([171, 205, 239])) + }) + test("converts hex string without 0x prefix to bytes", () => { const hex = "01020304" const bytes = hexStringToBytes(hex) diff --git a/packages/keys/src/encoding/hex.ts b/packages/keys/src/encoding/hex.ts index 559ce47..aa4de17 100644 --- a/packages/keys/src/encoding/hex.ts +++ b/packages/keys/src/encoding/hex.ts @@ -24,8 +24,10 @@ export function bytesToHexString(bytes: Uint8Array): string { * ``` */ export function hexStringToBytes(hex: string): Uint8Array { - const hexWithoutPrefix = hex.startsWith("0x") ? hex.slice(2) : hex - return fromString(hexWithoutPrefix, "base16") + const hexWithoutPrefix = hex.toLowerCase().startsWith("0x") + ? hex.slice(2) + : hex + return fromString(hexWithoutPrefix.toLowerCase(), "base16") } /** diff --git a/packages/keys/src/encoding/jwk.test.ts b/packages/keys/src/encoding/jwk.test.ts index 0db8432..db955da 100644 --- a/packages/keys/src/encoding/jwk.test.ts +++ b/packages/keys/src/encoding/jwk.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest" -import { bytesToBase64, isBase64 } from "./base64" +import { bytesToBase64url, isBase64url } from "./base64" import { bytesToJwk, isPrivateKeyJwk, isPublicKeyJwk, jwkToBytes } from "./jwk" import type { PublicKeyJwkEd25519, PublicKeyJwkSecp256k1 } from "./jwk" @@ -12,8 +12,8 @@ describe("JWK encoding", () => { secp256k1Bytes.fill(2, 33) // y-coordinate (32 bytes) // Generate the actual base64url encoded strings from our test data - const base64String = bytesToBase64(Ed25519Bytes) // base64url of 32 bytes of 1s - const base64String2 = bytesToBase64(secp256k1Bytes.slice(33)) // base64url of 32 bytes of 2s + const base64String = bytesToBase64url(Ed25519Bytes) // base64url of 32 bytes of 1s + const base64String2 = bytesToBase64url(secp256k1Bytes.slice(33)) // base64url of 32 bytes of 2s describe("bytesToJwk", () => { test("converts Ed25519 public key to JWK", () => { @@ -23,7 +23,7 @@ describe("JWK encoding", () => { crv: "Ed25519", x: expect.any(String) as unknown }) - expect(isBase64(jwk.x)).toBe(true) + expect(isBase64url(jwk.x)).toBe(true) }) test("converts secp256k1 public key to JWK", () => { @@ -37,8 +37,8 @@ describe("JWK encoding", () => { x: expect.any(String) as unknown, y: expect.any(String) as unknown }) - expect(isBase64(jwk.x)).toBe(true) - expect(isBase64(jwk.y)).toBe(true) + expect(isBase64url(jwk.x)).toBe(true) + expect(isBase64url(jwk.y)).toBe(true) }) }) diff --git a/packages/keys/src/encoding/jwk.ts b/packages/keys/src/encoding/jwk.ts index 1bfc1e7..f42ceca 100644 --- a/packages/keys/src/encoding/jwk.ts +++ b/packages/keys/src/encoding/jwk.ts @@ -1,4 +1,4 @@ -import { base64ToBytes, bytesToBase64 } from "./base64" +import { base64urlToBytes, bytesToBase64url } from "./base64" import type { KeypairAlgorithm } from "../types" /** @@ -26,43 +26,61 @@ export type PrivateKeyJwk = PublicKeyJwk & { d: string // base64url encoded private key } -/** - * Check if an object is a valid public key JWK - */ -export function isPublicKeyJwk(jwk: unknown): jwk is PublicKeyJwk { +export function isPublicKeyJwkSecp256k1( + jwk: unknown +): jwk is PublicKeyJwkSecp256k1 { if (typeof jwk !== "object" || jwk === null) { return false } const obj = jwk as Record - if (obj.kty !== "EC" && obj.kty !== "OKP") { + if (obj.kty !== "EC" || obj.crv !== "secp256k1") { return false } - if (obj.crv !== "secp256k1" && obj.crv !== "Ed25519") { + if (typeof obj.x !== "string" || obj.x.length === 0) { return false } - if (typeof obj.x !== "string" || obj.x.length === 0) { + if (typeof obj.y !== "string" || obj.y.length === 0) { + return false + } + + return true +} + +export function isPublicKeyJwkEd25519( + jwk: unknown +): jwk is PublicKeyJwkEd25519 { + if (typeof jwk !== "object" || jwk === null) { return false } - // For secp256k1, y coordinate is required - if (obj.crv === "secp256k1") { - if (typeof obj.y !== "string" || obj.y.length === 0) { - return false - } + const obj = jwk as Record + + if (obj.kty !== "OKP" || obj.crv !== "Ed25519") { + return false + } + + if (typeof obj.x !== "string" || obj.x.length === 0) { + return false } - // For Ed25519, y coordinate should not be present - if (obj.crv === "Ed25519" && "y" in obj) { + if ("y" in obj) { return false } return true } +/** + * Check if an object is a valid public key JWK + */ +export function isPublicKeyJwk(jwk: unknown): jwk is PublicKeyJwk { + return isPublicKeyJwkSecp256k1(jwk) || isPublicKeyJwkEd25519(jwk) +} + /** * Check if an object is a valid private key JWK */ @@ -86,7 +104,7 @@ export function bytesToJwk( return { kty: "OKP", crv: "Ed25519", - x: bytesToBase64(bytes) + x: bytesToBase64url(bytes) } as const } @@ -100,8 +118,8 @@ export function bytesToJwk( return { kty: "EC", crv: "secp256k1", - x: bytesToBase64(xBytes), - y: bytesToBase64(yBytes) + x: bytesToBase64url(xBytes), + y: bytesToBase64url(yBytes) } as const } @@ -109,14 +127,14 @@ export function bytesToJwk( * Convert a JWK to public key bytes */ export function jwkToBytes(jwk: PublicKeyJwk): Uint8Array { - const xBytes = base64ToBytes(jwk.x) + const xBytes = base64urlToBytes(jwk.x) // For secp256k1, we need to reconstruct the full public key if (jwk.crv === "secp256k1") { const fullKey = new Uint8Array(65) fullKey[0] = 0x04 // Add the prefix byte fullKey.set(xBytes, 1) // Add the x-coordinate - fullKey.set(base64ToBytes(jwk.y), 33) // Add the y-coordinate + fullKey.set(base64urlToBytes(jwk.y), 33) // Add the y-coordinate return fullKey } diff --git a/packages/keys/src/index.ts b/packages/keys/src/index.ts index 0be37af..87ec31a 100644 --- a/packages/keys/src/index.ts +++ b/packages/keys/src/index.ts @@ -1,3 +1,3 @@ -export * from "./types" export * from "./keypair" export * from "./public-key" +export * from "./types" diff --git a/packages/keys/src/keypair.ts b/packages/keys/src/keypair.ts index fbded2d..fc896d8 100644 --- a/packages/keys/src/keypair.ts +++ b/packages/keys/src/keypair.ts @@ -1,10 +1,9 @@ import { generateKeypair as ed25519 } from "./curves/ed25519" import { generateKeypair as secp256k1 } from "./curves/secp256k1" -import { base58ToBytes, bytesToBase58 } from "./encoding/base58" -import { base64ToBytes, bytesToBase64 } from "./encoding/base64" +import { base64urlToBytes, bytesToBase64url } from "./encoding/base64" import { bytesToJwk, jwkToBytes } from "./encoding/jwk" import type { PrivateKeyJwk } from "./encoding/jwk" -import type { Keypair, KeypairAlgorithm, KeypairBase58 } from "./types" +import type { Keypair, KeypairAlgorithm } from "./types" /** * Generate a Keypair for a given algorithm @@ -24,34 +23,6 @@ export async function generateKeypair( return ed25519(privateKeyBytes) } -/** - * Convert a Keypair to a base58-encoded string format - * - * @param keypair - The Keypair to convert - * @returns A base58-encoded string representation of the Keypair - */ -export function keypairToBase58(keypair: Keypair): KeypairBase58 { - return { - publicKey: bytesToBase58(keypair.publicKey), - privateKey: bytesToBase58(keypair.privateKey), - algorithm: keypair.algorithm - } -} - -/** - * Convert a base58-encoded string format back to a Keypair - * - * @param base58 - The base58-encoded Keypair - * @returns The reconstructed Keypair - */ -export function keypairFromBase58(base58: KeypairBase58): Keypair { - return { - publicKey: base58ToBytes(base58.publicKey), - privateKey: base58ToBytes(base58.privateKey), - algorithm: base58.algorithm - } -} - /** * Convert a Keypair to a JWK format * @@ -62,7 +33,7 @@ export function keypairToJwk(keypair: Keypair): PrivateKeyJwk { const publicKeyJwk = bytesToJwk(keypair.publicKey, keypair.algorithm) return { ...publicKeyJwk, - d: bytesToBase64(keypair.privateKey) + d: bytesToBase64url(keypair.privateKey) } } @@ -75,7 +46,7 @@ export function keypairToJwk(keypair: Keypair): PrivateKeyJwk { export function jwkToKeypair(jwk: PrivateKeyJwk): Keypair { return { publicKey: jwkToBytes(jwk), - privateKey: base64ToBytes(jwk.d), + privateKey: base64urlToBytes(jwk.d), algorithm: jwk.crv } } diff --git a/packages/keys/src/public-key.test.ts b/packages/keys/src/public-key.test.ts index 3c4ed56..16f5f83 100644 --- a/packages/keys/src/public-key.test.ts +++ b/packages/keys/src/public-key.test.ts @@ -1,64 +1,64 @@ import { describe, expect, test } from "vitest" import { isBase58 } from "./encoding/base58" -import { base64ToBytes, isBase64 } from "./encoding/base64" +import { base64urlToBytes, isBase64url } from "./encoding/base64" import { isHexString } from "./encoding/hex" +import { isPublicKeyJwkEd25519, isPublicKeyJwkSecp256k1 } from "./encoding/jwk" import { isMultibase } from "./encoding/multibase" import { generateKeypair } from "./keypair" -import { - formatPublicKey, - formatPublicKeyBase58, - formatPublicKeyHex, - formatPublicKeyJwk, - formatPublicKeyMultibase, - publicKeyFormats -} from "./public-key" -import type { PublicKeyJwkEd25519, PublicKeyJwkSecp256k1 } from "./encoding/jwk" +import { encodePublicKeyFromKeypair, publicKeyEncodings } from "./public-key" const keypairAlgorithms = ["secp256k1", "Ed25519"] as const describe("public key encoding", () => { describe.each(keypairAlgorithms)("algorithm: %s", (algorithm) => { - describe.each(publicKeyFormats)("format: %s", (format) => { - test("formats public key correctly", async () => { + describe.each(publicKeyEncodings)("format: %s", (format) => { + test("encodes public key correctly", async () => { const keypair = await generateKeypair(algorithm) - const result = formatPublicKey(keypair, format) + const publicKey = encodePublicKeyFromKeypair(format, keypair) + const publicKeyValue = publicKey.value switch (format) { case "hex": - expect(isHexString(result)).toBe(true) + expect(isHexString(publicKeyValue)).toBe(true) break case "jwk": if (algorithm === "secp256k1") { - const jwk = result as PublicKeyJwkSecp256k1 - expect(jwk).toEqual({ + if (!isPublicKeyJwkSecp256k1(publicKeyValue)) { + throw new Error("Invalid JWK") + } + + expect(publicKeyValue).toEqual({ kty: "EC", crv: "secp256k1", x: expect.any(String) as unknown, y: expect.any(String) as unknown }) - expect(isBase64(jwk.x)).toBe(true) - expect(isBase64(jwk.y)).toBe(true) - const xBytes = base64ToBytes(jwk.x) + expect(isBase64url(publicKeyValue.x)).toBe(true) + expect(isBase64url(publicKeyValue.y)).toBe(true) + const xBytes = base64urlToBytes(publicKeyValue.x) expect(xBytes.length).toBe(32) - const yBytes = base64ToBytes(jwk.y) + const yBytes = base64urlToBytes(publicKeyValue.y) expect(yBytes.length).toBe(32) } else { - const jwk = result as PublicKeyJwkEd25519 - expect(jwk).toEqual({ + if (!isPublicKeyJwkEd25519(publicKeyValue)) { + throw new Error("Invalid JWK") + } + + expect(publicKeyValue).toEqual({ kty: "OKP", crv: "Ed25519", x: expect.any(String) as unknown }) - expect(isBase64(jwk.x)).toBe(true) - const xBytes = base64ToBytes(jwk.x) + expect(isBase64url(publicKeyValue.x)).toBe(true) + const xBytes = base64urlToBytes(publicKeyValue.x) expect(xBytes.length).toBe(32) } break case "multibase": - expect(isMultibase(result)).toBe(true) + expect(isMultibase(publicKeyValue)).toBe(true) break case "base58": - expect(isBase58(result)).toBe(true) + expect(isBase58(publicKeyValue)).toBe(true) break } }) @@ -69,22 +69,22 @@ describe("public key encoding", () => { describe.each(keypairAlgorithms)("algorithm: %s", (algorithm) => { test("formats to multibase", async () => { const keypair = await generateKeypair(algorithm) - const multibase = formatPublicKeyMultibase(keypair) - expect(isMultibase(multibase)).toBe(true) + const multibase = encodePublicKeyFromKeypair("multibase", keypair) + expect(isMultibase(multibase.value)).toBe(true) }) test("formats to JWK", async () => { const keypair = await generateKeypair(algorithm) - const jwk = formatPublicKeyJwk(keypair) + const jwk = encodePublicKeyFromKeypair("jwk", keypair) if (algorithm === "secp256k1") { - expect(jwk).toEqual({ + expect(jwk.value).toEqual({ kty: "EC", crv: "secp256k1", x: expect.any(String) as unknown, y: expect.any(String) as unknown }) } else { - expect(jwk).toEqual({ + expect(jwk.value).toEqual({ kty: "OKP", crv: "Ed25519", x: expect.any(String) as unknown @@ -94,14 +94,14 @@ describe("public key encoding", () => { test("formats to hex", async () => { const keypair = await generateKeypair(algorithm) - const hex = formatPublicKeyHex(keypair) - expect(isHexString(hex)).toBe(true) + const hex = encodePublicKeyFromKeypair("hex", keypair) + expect(isHexString(hex.value)).toBe(true) }) test("formats to base58", async () => { const keypair = await generateKeypair(algorithm) - const base58 = formatPublicKeyBase58(keypair) - expect(isBase58(base58)).toBe(true) + const base58 = encodePublicKeyFromKeypair("base58", keypair) + expect(isBase58(base58.value)).toBe(true) }) }) }) diff --git a/packages/keys/src/public-key.ts b/packages/keys/src/public-key.ts index 8435e6b..e9a0856 100644 --- a/packages/keys/src/public-key.ts +++ b/packages/keys/src/public-key.ts @@ -4,13 +4,13 @@ import { bytesToHexString } from "./encoding/hex" import { bytesToJwk } from "./encoding/jwk" import { bytesToMultibase } from "./encoding/multibase" import type { PublicKeyJwk } from "./encoding/jwk" -import type { Keypair } from "./types" +import type { Keypair, KeypairAlgorithm } from "./types" /** * Public key format types */ -export const publicKeyFormats = ["hex", "jwk", "multibase", "base58"] as const -export type PublicKeyFormat = (typeof publicKeyFormats)[number] +export const publicKeyEncodings = ["hex", "jwk", "multibase", "base58"] as const +export type PublicKeyEncoding = (typeof publicKeyEncodings)[number] export type PublicKeyTypeMap = { hex: string jwk: PublicKeyJwk @@ -18,6 +18,22 @@ export type PublicKeyTypeMap = { base58: string } +/** + * A type that represents a PublicKey with its encoding format and algorithm + */ +export type PublicKeyWithEncoding = { + [K in PublicKeyEncoding]: { + encoding: K + algorithm: KeypairAlgorithm + value: PublicKeyTypeMap[K] + } +}[PublicKeyEncoding] + +/** + * Get the compressed public key for a given keypair + * @param keypair - The keypair to get the compressed public key for + * @returns The compressed public key + */ export function getCompressedPublicKey(keypair: Keypair): Uint8Array { if (keypair.algorithm === "secp256k1") { return compressPublicKey(keypair) @@ -28,55 +44,92 @@ export function getCompressedPublicKey(keypair: Keypair): Uint8Array { /** * Convert a public key to a multibase string (used for DID:key) - * @param keypair - The Keypair containing the public key - * @returns A multibase string representation of the public key */ -export function formatPublicKeyMultibase(keypair: Keypair): string { - return bytesToMultibase(keypair.publicKey) +function encodePublicKeyMultibase( + publicKey: Uint8Array, + algorithm: KeypairAlgorithm +): PublicKeyWithEncoding & { encoding: "multibase" } { + return { + encoding: "multibase", + algorithm, + value: bytesToMultibase(publicKey) + } } /** * Convert a public key to a JWK format - * @param keypair - The Keypair containing the public key - * @returns A JSON Web Key representation of the public key */ -export function formatPublicKeyJwk(keypair: Keypair): PublicKeyJwk { - return bytesToJwk(keypair.publicKey, keypair.algorithm) +function encodePublicKeyJwk( + publicKey: Uint8Array, + algorithm: KeypairAlgorithm +): PublicKeyWithEncoding & { encoding: "jwk" } { + return { + encoding: "jwk", + algorithm, + value: bytesToJwk(publicKey, algorithm) + } } /** * Convert a public key to a hex string - * @param keypair - The Keypair containing the public key - * @returns A hex string representation of the public key */ -export function formatPublicKeyHex(keypair: Keypair): string { - return bytesToHexString(keypair.publicKey) +function encodePublicKeyHex( + publicKey: Uint8Array, + algorithm: KeypairAlgorithm +): PublicKeyWithEncoding & { encoding: "hex" } { + return { + encoding: "hex", + algorithm, + value: bytesToHexString(publicKey) + } } /** * Convert a public key to a base58 string - * @param keypair - The Keypair containing the public key - * @returns A base58 string representation of the public key */ -export function formatPublicKeyBase58(keypair: Keypair): string { - return bytesToBase58(keypair.publicKey) +function encodePublicKeyBase58( + publicKey: Uint8Array, + algorithm: KeypairAlgorithm +): PublicKeyWithEncoding & { encoding: "base58" } { + return { + encoding: "base58", + algorithm, + value: bytesToBase58(publicKey) + } } -export const publicKeyFormatters = { - hex: (keypair: Keypair) => formatPublicKeyHex(keypair), - jwk: (keypair: Keypair) => formatPublicKeyJwk(keypair), - multibase: (keypair: Keypair) => formatPublicKeyMultibase(keypair), - base58: (keypair: Keypair) => formatPublicKeyBase58(keypair) +/** + * A map of public key encoders + */ +const publicKeyEncoders: { + [K in PublicKeyEncoding]: ( + publicKey: Uint8Array, + algorithm: KeypairAlgorithm + ) => PublicKeyWithEncoding & { encoding: K } +} = { + hex: encodePublicKeyHex, + jwk: encodePublicKeyJwk, + multibase: encodePublicKeyMultibase, + base58: encodePublicKeyBase58 } as const /** - * Convert a public key to the specified format with correct type inference + * Encode a raw public key to the specified format + */ +export function encodePublicKey( + encoding: T, + publicKey: Uint8Array, + algorithm: KeypairAlgorithm +): PublicKeyWithEncoding & { encoding: T } { + return publicKeyEncoders[encoding](publicKey, algorithm) +} + +/** + * Encode a public key from a keypair to the specified format */ -export function formatPublicKey( - keypair: Keypair, - format: T -): ReturnType<(typeof publicKeyFormatters)[T]> { - return publicKeyFormatters[format](keypair) as ReturnType< - (typeof publicKeyFormatters)[T] - > +export function encodePublicKeyFromKeypair( + encoding: T, + keypair: Keypair +): PublicKeyWithEncoding & { encoding: T } { + return encodePublicKey(encoding, keypair.publicKey, keypair.algorithm) }