anon-identity
Version:
Decentralized identity framework with DIDs, Verifiable Credentials, and privacy-preserving selective disclosure
446 lines • 19.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HybridStorageProvider = void 0;
const storage_factory_1 = require("../storage-factory");
const uuid_1 = require("uuid");
class HybridStorageProvider {
constructor(config) {
this.dataIndex = new Map();
this.config = config;
// Initialize providers based on configuration
this.providers = {
local: storage_factory_1.StorageFactory.createProvider({ provider: 'memory' }), // Always have local
};
if (config.blockchain) {
this.providers.blockchain = storage_factory_1.StorageFactory.createProvider({
provider: 'blockchain',
blockchain: config.blockchain,
cache: config.cache,
});
}
if (config.ipfs) {
this.providers.ipfs = storage_factory_1.StorageFactory.createProvider({
provider: 'ipfs',
ipfs: config.ipfs,
});
}
// Start synchronization if enabled
if (config.hybrid?.sync?.enabled) {
this.startSync();
}
}
// Routing logic
getProviderForDataType(dataType, dataSize) {
const routing = this.config.hybrid?.routing;
const thresholds = this.config.hybrid?.sizeThresholds;
const providers = [];
// Check explicit routing configuration
if (routing) {
const explicitRoute = routing[`${dataType}s`];
if (explicitRoute && this.providers[explicitRoute]) {
providers.push(explicitRoute);
}
}
// If no explicit routing, use intelligent routing based on data characteristics
if (providers.length === 0) {
switch (dataType) {
case 'did':
// DIDs are small and need high availability - blockchain + local
if (this.providers.blockchain)
providers.push('blockchain');
providers.push('local');
break;
case 'revocation':
// Revocations must be publicly verifiable - blockchain
if (this.providers.blockchain) {
providers.push('blockchain');
}
else if (this.providers.ipfs) {
providers.push('ipfs');
}
providers.push('local');
break;
case 'schema':
// Schemas are public and rarely change - IPFS + blockchain
if (this.providers.ipfs)
providers.push('ipfs');
if (this.providers.blockchain)
providers.push('blockchain');
providers.push('local');
break;
case 'credential':
// Route based on size if available
if (dataSize && thresholds) {
if (dataSize > (thresholds.useIPFS || 10240) && this.providers.ipfs) {
providers.push('ipfs');
}
else if (dataSize < (thresholds.useLocal || 1024)) {
providers.push('local');
}
else if (this.providers.blockchain) {
providers.push('blockchain');
}
}
else {
// Default: IPFS for full data, blockchain for hash
if (this.providers.ipfs)
providers.push('ipfs');
if (this.providers.blockchain)
providers.push('blockchain');
providers.push('local');
}
break;
}
}
// Always ensure at least local storage
if (!providers.includes('local')) {
providers.push('local');
}
return providers;
}
// Execute operation with fallback support
async executeWithFallback(operation, providers, fallbackOperation) {
const fallbackConfig = this.config.hybrid?.fallback;
const maxRetries = fallbackConfig?.retries || 3;
const retryDelay = fallbackConfig?.retryDelay || 1000;
let lastError = null;
for (const providerType of providers) {
const provider = this.providers[providerType];
if (!provider)
continue;
for (let retry = 0; retry < maxRetries; retry++) {
try {
return await operation(provider);
}
catch (error) {
lastError = error;
console.warn(`Provider ${providerType} failed (retry ${retry + 1}/${maxRetries}):`, error);
if (retry < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, retryDelay * (retry + 1)));
}
}
}
// Try fallback operation if main operation failed
if (fallbackOperation && fallbackConfig?.enabled) {
try {
return await fallbackOperation(provider);
}
catch (fallbackError) {
console.warn(`Fallback operation also failed for ${providerType}:`, fallbackError);
}
}
}
throw lastError || new Error('All storage providers failed');
}
// Track where data is stored
trackDataLocation(key, providers) {
if (!this.dataIndex.has(key)) {
this.dataIndex.set(key, new Set());
}
providers.forEach(p => this.dataIndex.get(key).add(p));
}
// DID Operations
async storeDID(did, document) {
const providers = this.getProviderForDataType('did');
const key = `did:${did}`;
// Store in all designated providers
const storePromises = providers.map(providerType => {
const provider = this.providers[providerType];
if (provider) {
return provider.storeDID(did, document)
.then(() => this.trackDataLocation(key, [providerType]))
.catch(err => console.error(`Failed to store DID in ${providerType}:`, err));
}
return Promise.resolve();
});
await Promise.all(storePromises);
}
async resolveDID(did) {
const key = `did:${did}`;
const locations = this.dataIndex.get(key) || new Set(this.getProviderForDataType('did'));
return this.executeWithFallback(async (provider) => provider.resolveDID(did), Array.from(locations));
}
async listDIDs(owner) {
// Aggregate from all providers
const allDIDs = new Set();
for (const [providerType, provider] of Object.entries(this.providers)) {
if (provider) {
try {
const dids = await provider.listDIDs(owner);
dids.forEach(did => allDIDs.add(did));
}
catch (error) {
console.warn(`Failed to list DIDs from ${providerType}:`, error);
}
}
}
return Array.from(allDIDs);
}
// Credential Operations
async storeCredential(credential) {
const dataSize = JSON.stringify(credential).length;
const providers = this.getProviderForDataType('credential', dataSize);
const key = `credential:${credential.id}`;
// For hybrid storage, we might store full data in IPFS and hash on blockchain
if (providers.includes('blockchain') && providers.includes('ipfs')) {
// Store full credential in IPFS
if (this.providers.ipfs) {
await this.providers.ipfs.storeCredential(credential);
}
// Store only hash/reference on blockchain
if (this.providers.blockchain) {
// The blockchain provider already handles this by storing hashes
await this.providers.blockchain.storeCredential(credential);
}
this.trackDataLocation(key, ['blockchain', 'ipfs']);
}
else {
// Store in all designated providers
const storePromises = providers.map(providerType => {
const provider = this.providers[providerType];
if (provider) {
return provider.storeCredential(credential)
.then(() => this.trackDataLocation(key, [providerType]))
.catch(err => console.error(`Failed to store credential in ${providerType}:`, err));
}
return Promise.resolve();
});
await Promise.all(storePromises);
}
}
async getCredential(id) {
const key = `credential:${id}`;
const locations = this.dataIndex.get(key) || new Set(this.getProviderForDataType('credential'));
// Prefer IPFS for full credential data
const orderedLocations = Array.from(locations).sort((a, b) => {
if (a === 'ipfs')
return -1;
if (b === 'ipfs')
return 1;
return 0;
});
return this.executeWithFallback(async (provider) => provider.getCredential(id), orderedLocations);
}
async listCredentials(holder) {
// Aggregate from all providers, removing duplicates
const credentialsMap = new Map();
for (const [providerType, provider] of Object.entries(this.providers)) {
if (provider) {
try {
const credentials = await provider.listCredentials(holder);
credentials.forEach(cred => credentialsMap.set(cred.id, cred));
}
catch (error) {
console.warn(`Failed to list credentials from ${providerType}:`, error);
}
}
}
return Array.from(credentialsMap.values());
}
async deleteCredential(id) {
const key = `credential:${id}`;
const locations = this.dataIndex.get(key) || new Set(['local']);
// Delete from all locations
const deletePromises = Array.from(locations).map(providerType => {
const provider = this.providers[providerType];
if (provider && providerType !== 'blockchain') { // Can't delete from blockchain
return provider.deleteCredential(id)
.catch(err => console.error(`Failed to delete credential from ${providerType}:`, err));
}
return Promise.resolve();
});
await Promise.all(deletePromises);
this.dataIndex.delete(key);
}
// Revocation Operations
async publishRevocation(issuerDID, revocationList) {
const providers = this.getProviderForDataType('revocation');
const key = `revocation:${issuerDID}`;
// Revocations should be on blockchain for public verifiability
const storePromises = providers.map(providerType => {
const provider = this.providers[providerType];
if (provider) {
return provider.publishRevocation(issuerDID, revocationList)
.then(() => this.trackDataLocation(key, [providerType]))
.catch(err => console.error(`Failed to publish revocation in ${providerType}:`, err));
}
return Promise.resolve();
});
await Promise.all(storePromises);
}
async checkRevocation(issuerDID, credentialId) {
const key = `revocation:${issuerDID}`;
const locations = this.dataIndex.get(key) || new Set(this.getProviderForDataType('revocation'));
// Check blockchain first for most authoritative answer
const orderedLocations = Array.from(locations).sort((a, b) => {
if (a === 'blockchain')
return -1;
if (b === 'blockchain')
return 1;
return 0;
});
try {
return await this.executeWithFallback(async (provider) => provider.checkRevocation(issuerDID, credentialId), orderedLocations);
}
catch {
return false; // Default to not revoked if check fails
}
}
async getRevocationList(issuerDID) {
const key = `revocation:${issuerDID}`;
const locations = this.dataIndex.get(key) || new Set(this.getProviderForDataType('revocation'));
return this.executeWithFallback(async (provider) => provider.getRevocationList(issuerDID), Array.from(locations));
}
// Key Management (always local)
async storeKeyPair(identifier, encryptedKeyPair) {
return this.providers.local.storeKeyPair(identifier, encryptedKeyPair);
}
async retrieveKeyPair(identifier) {
return this.providers.local.retrieveKeyPair(identifier);
}
async deleteKeyPair(identifier) {
return this.providers.local.deleteKeyPair(identifier);
}
// Schema Operations
async registerSchema(schema) {
const providers = this.getProviderForDataType('schema');
const schemaId = schema.id || `schema:${(0, uuid_1.v4)()}`;
const key = `schema:${schemaId}`;
// Store in all designated providers
let resultId = schemaId;
for (const providerType of providers) {
const provider = this.providers[providerType];
if (provider) {
try {
const id = await provider.registerSchema({ ...schema, id: schemaId });
resultId = id;
this.trackDataLocation(key, [providerType]);
}
catch (err) {
console.error(`Failed to register schema in ${providerType}:`, err);
}
}
}
return resultId;
}
async getSchema(schemaId) {
const key = `schema:${schemaId}`;
const locations = this.dataIndex.get(key) || new Set(this.getProviderForDataType('schema'));
return this.executeWithFallback(async (provider) => provider.getSchema(schemaId), Array.from(locations));
}
async listSchemas(issuerDID) {
// Aggregate from all providers, removing duplicates
const schemasMap = new Map();
for (const [providerType, provider] of Object.entries(this.providers)) {
if (provider) {
try {
const schemas = await provider.listSchemas(issuerDID);
schemas.forEach(schema => {
if (schema.id) {
schemasMap.set(schema.id, schema);
}
});
}
catch (error) {
console.warn(`Failed to list schemas from ${providerType}:`, error);
}
}
}
return Array.from(schemasMap.values());
}
// General operations
async clear() {
// Clear all providers
const clearPromises = Object.values(this.providers).map(provider => provider?.clear().catch(err => console.error('Failed to clear provider:', err)));
await Promise.all(clearPromises);
this.dataIndex.clear();
}
// Synchronization
startSync() {
const interval = this.config.hybrid?.sync?.interval || 60000; // Default 1 minute
this.syncInterval = setInterval(() => {
this.performSync().catch(err => console.error('Synchronization failed:', err));
}, interval);
}
async performSync() {
console.log('Starting hybrid storage synchronization...');
// Sync DIDs
const allDIDs = await this.listDIDs();
for (const did of allDIDs) {
await this.syncData('did', did);
}
// Additional sync operations can be added here
console.log('Synchronization completed');
}
async syncData(dataType, id) {
const key = `${dataType}:${id}`;
const locations = this.dataIndex.get(key);
if (!locations || locations.size <= 1) {
return; // No need to sync if data is in only one location
}
// Implement conflict resolution based on configuration
const conflictResolution = this.config.hybrid?.sync?.conflictResolution || 'newest';
// This is a simplified sync - in production, you'd implement
// proper conflict resolution and data comparison
console.log(`Syncing ${key} across ${locations.size} locations`);
}
// Phone Number Operations
async storePhoneNumber(userDID, phoneNumber) {
// Route to local storage for privacy-sensitive data
return this.providers.local.storePhoneNumber(userDID, phoneNumber);
}
async getPhoneNumber(userDID, phoneId) {
return this.providers.local.getPhoneNumber(userDID, phoneId);
}
async listPhoneNumbers(userDID) {
return this.providers.local.listPhoneNumbers(userDID);
}
async updatePhoneNumber(userDID, phoneId, phoneNumber) {
return this.providers.local.updatePhoneNumber(userDID, phoneId, phoneNumber);
}
async deletePhoneNumber(userDID, phoneId) {
return this.providers.local.deletePhoneNumber(userDID, phoneId);
}
// Address Operations
async storeAddress(userDID, address) {
// Route to local storage for privacy-sensitive data
return this.providers.local.storeAddress(userDID, address);
}
async getAddress(userDID, addressId) {
return this.providers.local.getAddress(userDID, addressId);
}
async listAddresses(userDID) {
return this.providers.local.listAddresses(userDID);
}
async updateAddress(userDID, addressId, address) {
return this.providers.local.updateAddress(userDID, addressId, address);
}
async deleteAddress(userDID, addressId) {
return this.providers.local.deleteAddress(userDID, addressId);
}
// Email Address Operations
async storeEmailAddress(userDID, emailAddress) {
// Route to local storage for privacy-sensitive data
return this.providers.local.storeEmailAddress(userDID, emailAddress);
}
async getEmailAddress(userDID, emailId) {
return this.providers.local.getEmailAddress(userDID, emailId);
}
async listEmailAddresses(userDID) {
return this.providers.local.listEmailAddresses(userDID);
}
async updateEmailAddress(userDID, emailId, emailAddress) {
return this.providers.local.updateEmailAddress(userDID, emailId, emailAddress);
}
async deleteEmailAddress(userDID, emailId) {
return this.providers.local.deleteEmailAddress(userDID, emailId);
}
// Cleanup
destroy() {
if (this.syncInterval) {
clearInterval(this.syncInterval);
}
}
}
exports.HybridStorageProvider = HybridStorageProvider;
//# sourceMappingURL=hybrid-storage-provider.js.map