@zipwire/proofpack
Version:
Core ProofPack verifiable data exchange format with JWS, Merkle trees, selective disclosure, and blockchain attestation support
245 lines (220 loc) • 8.48 kB
JavaScript
import { JwsEnvelopeBuilder } from './JwsEnvelopeBuilder.js';
import { JwsSerializerOptions } from './JwsSerializerOptions.js';
/**
* AttestationLocator - Locates an attestation on a blockchain
*
* @typedef {Object} AttestationLocator
* @property {string} serviceId - The attestation service ID (e.g., 'eas', 'fake-attestation-service')
* @property {string} network - The blockchain network (e.g., 'base-sepolia')
* @property {string} schemaId - The schema ID (e.g., '0xdeadbeef')
* @property {string} attestationId - The attestation ID (e.g., '0xbeefdead')
* @property {string} attesterAddress - The attester's address (e.g., '0x01020304')
* @property {string} recipientAddress - The recipient's address (e.g., '0x10203040')
*/
/**
* AttestedMerkleExchangeBuilder - Builds attested Merkle proofs
*
* Follows the .NET pattern:
* - Builder pattern with fluent API
* - Uses JwsEnvelopeBuilder for signing
* - Adds blockchain attestation to Merkle proofs
* - Supports multiple attestation services
* - Supports multiple signers
*/
class AttestedMerkleExchangeBuilder {
/**
* Creates a new builder from a Merkle tree
* @param {MerkleTree} merkleTree - The Merkle tree to build a proof for
* @returns {AttestedMerkleExchangeBuilder} A new builder
*/
static fromMerkleTree(merkleTree) {
if (!merkleTree) {
throw new Error('MerkleTree is required');
}
return new AttestedMerkleExchangeBuilder(merkleTree);
}
/**
* Constructor
* @param {MerkleTree} merkleTree - The Merkle tree
* @private
*/
constructor(merkleTree) {
this.merkleTree = merkleTree;
this.attestationLocator = null;
this.nonce = null;
this.issuedTo = null;
}
/**
* Adds an attestation locator to the builder
* @param {AttestationLocator} attestationLocator - The attestation locator
* @returns {AttestedMerkleExchangeBuilder} The builder for chaining
*/
withAttestation(attestationLocator) {
if (!attestationLocator) {
throw new Error('AttestationLocator is required');
}
// Validate required properties
const requiredProps = ['serviceId', 'network', 'schemaId', 'attestationId', 'attesterAddress', 'recipientAddress'];
for (const prop of requiredProps) {
if (!attestationLocator[prop]) {
throw new Error(`AttestationLocator.${prop} is required`);
}
}
this.attestationLocator = attestationLocator;
return this;
}
/**
* Sets the nonce
* @param {string} nonce - The nonce. If not provided, a random nonce will be generated
* @returns {AttestedMerkleExchangeBuilder} The builder for chaining
*/
withNonce(nonce = null) {
if (nonce === null) {
nonce = AttestedMerkleExchangeBuilder.generateNonce();
}
this.nonce = nonce;
return this;
}
/**
* Sets the issued to identifiers
* @param {string|object} keyOrObject - Either a key string or an object with key-value pairs
* @param {string} [value] - The value when first parameter is a key string
* @returns {AttestedMerkleExchangeBuilder} The builder for chaining
*/
withIssuedTo(keyOrObject, value = undefined) {
if (typeof keyOrObject === 'string') {
if (value === undefined) {
throw new Error('Value is required when key is provided as string');
}
if (typeof value !== 'string') {
throw new Error('Value must be a string');
}
if (!this.issuedTo) {
this.issuedTo = {};
}
this.issuedTo[keyOrObject] = value;
} else if (typeof keyOrObject === 'object' && keyOrObject !== null) {
if (value !== undefined) {
throw new Error('Value parameter should not be provided when first parameter is an object');
}
// Validate all values are strings
for (const [key, val] of Object.entries(keyOrObject)) {
if (typeof key !== 'string' || typeof val !== 'string') {
throw new Error('All keys and values must be strings');
}
}
this.issuedTo = { ...keyOrObject };
} else {
throw new Error('First parameter must be a string key or an object with key-value pairs');
}
return this;
}
/**
* Sets the issued to email address
* @param {string} email - The email address
* @returns {AttestedMerkleExchangeBuilder} The builder for chaining
*/
withIssuedToEmail(email) {
if (typeof email !== 'string' || !email) {
throw new Error('Email must be a non-empty string');
}
return this.withIssuedTo('email', email);
}
/**
* Sets the issued to phone number
* @param {string} phone - The phone number
* @returns {AttestedMerkleExchangeBuilder} The builder for chaining
*/
withIssuedToPhone(phone) {
if (typeof phone !== 'string' || !phone) {
throw new Error('Phone must be a non-empty string');
}
return this.withIssuedTo('phone', phone);
}
/**
* Sets the issued to Ethereum address
* @param {string} address - The Ethereum address
* @returns {AttestedMerkleExchangeBuilder} The builder for chaining
*/
withIssuedToEthereum(address) {
if (typeof address !== 'string' || !address) {
throw new Error('Address must be a non-empty string');
}
return this.withIssuedTo('ethereum', address);
}
/**
* Builds a payload (POJO)
* @returns {object} The payload object
*/
buildPayload() {
if (!this.attestationLocator) {
throw new Error('Attestation locator is required');
}
// Validate supported attestation service
const supportedServices = ['eas', 'fake-attestation-service'];
if (!supportedServices.includes(this.attestationLocator.serviceId.toLowerCase())) {
throw new Error(`Unsupported attestation service '${this.attestationLocator.serviceId}'`);
}
const nonce = this.nonce || AttestedMerkleExchangeBuilder.generateNonce();
// Create EAS schema
const schema = {
schemaUid: this.attestationLocator.schemaId,
name: 'PrivateData'
};
// Create EAS attestation
const easAttestation = {
network: this.attestationLocator.network,
attestationUid: this.attestationLocator.attestationId,
from: this.attestationLocator.attesterAddress,
to: this.attestationLocator.recipientAddress,
schema: schema
};
// Create attestation wrapper
const attestation = {
eas: easAttestation
};
const payload = {
merkleTree: this.merkleTree,
attestation: attestation,
timestamp: new Date().toISOString(),
nonce: nonce
};
if (this.issuedTo) {
payload.issuedTo = this.issuedTo;
}
return payload;
}
/**
* Builds a signed JWS envelope containing the attested Merkle proof
* @param {object} signer - The signing context to use
* @returns {Promise<object>} The signed JWS envelope
*/
async buildSigned(signer) {
return this.buildSignedMultiple([signer]);
}
/**
* Builds a signed JWS envelope containing the attested Merkle proof with multiple signers
* @param {object[]} signers - The signing contexts to use
* @returns {Promise<object>} The signed JWS envelope
*/
async buildSignedMultiple(signers) {
const builder = new JwsEnvelopeBuilder(
signers,
'JWT',
'application/attested-merkle-exchange+json'
);
return await builder.build(this.buildPayload());
}
/**
* Generates a new nonce as a GUID without dashes
* @returns {string} A new nonce string
*/
static generateNonce() {
// Generate a GUID-like string without dashes (32 characters)
return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/[x]/g, () => {
const r = Math.random() * 16 | 0;
return r.toString(16);
});
}
}
export { AttestedMerkleExchangeBuilder };