- Add Legal Office of the Master seal (SVG design with Maltese Cross, scales of justice, legal scroll) - Create legal-office-manifest-template.json for Legal Office credentials - Update SEAL_MAPPING.md and DESIGN_GUIDE.md with Legal Office seal documentation - Complete Azure CDN infrastructure deployment: - Resource group, storage account, and container created - 17 PNG seal files uploaded to Azure Blob Storage - All manifest templates updated with Azure URLs - Configuration files generated (azure-cdn-config.env) - Add comprehensive Azure CDN setup scripts and documentation - Fix manifest URL generation to prevent double slashes - Verify all seals accessible via HTTPS
181 lines
5.3 KiB
TypeScript
181 lines
5.3 KiB
TypeScript
/**
|
|
* Enhanced Microsoft Entra VerifiedID connector
|
|
* Adds retry logic, multi-manifest support, and improved error handling
|
|
*/
|
|
|
|
import { EntraVerifiedIDClient, EntraVerifiedIDConfig, VerifiableCredentialRequest, VerifiableCredentialResponse, VerifiableCredentialStatus, VerifiedCredential } from './entra-verifiedid';
|
|
|
|
export interface RetryConfig {
|
|
maxRetries?: number;
|
|
initialDelayMs?: number;
|
|
maxDelayMs?: number;
|
|
backoffMultiplier?: number;
|
|
retryableStatusCodes?: number[];
|
|
}
|
|
|
|
export interface MultiManifestConfig extends EntraVerifiedIDConfig {
|
|
manifests?: Record<string, string>; // manifest name -> manifest ID mapping
|
|
}
|
|
|
|
const DEFAULT_RETRY_CONFIG: Required<RetryConfig> = {
|
|
maxRetries: 3,
|
|
initialDelayMs: 1000,
|
|
maxDelayMs: 10000,
|
|
backoffMultiplier: 2,
|
|
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
};
|
|
|
|
/**
|
|
* Sleep utility for retry delays
|
|
*/
|
|
function sleep(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
/**
|
|
* Check if an error is retryable
|
|
*/
|
|
function isRetryableError(statusCode: number, retryableStatusCodes: number[]): boolean {
|
|
return retryableStatusCodes.includes(statusCode);
|
|
}
|
|
|
|
/**
|
|
* Enhanced Entra VerifiedID client with retry logic and multi-manifest support
|
|
*/
|
|
export class EnhancedEntraVerifiedIDClient extends EntraVerifiedIDClient {
|
|
private retryConfig: Required<RetryConfig>;
|
|
private manifests: Record<string, string>;
|
|
|
|
constructor(config: MultiManifestConfig, retryConfig?: RetryConfig) {
|
|
super(config);
|
|
this.retryConfig = { ...DEFAULT_RETRY_CONFIG, ...retryConfig };
|
|
this.manifests = config.manifests || {};
|
|
// Add default manifest if provided
|
|
if (config.credentialManifestId) {
|
|
this.manifests['default'] = config.credentialManifestId;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get manifest ID by name, fallback to default
|
|
*/
|
|
private getManifestId(manifestName?: string): string {
|
|
if (manifestName && this.manifests[manifestName]) {
|
|
return this.manifests[manifestName];
|
|
}
|
|
if (this.manifests['default']) {
|
|
return this.manifests['default'];
|
|
}
|
|
throw new Error('No credential manifest ID configured');
|
|
}
|
|
|
|
/**
|
|
* Execute a request with retry logic
|
|
*/
|
|
private async executeWithRetry<T>(
|
|
operation: () => Promise<T>,
|
|
operationName: string
|
|
): Promise<T> {
|
|
let lastError: Error | null = null;
|
|
let delay = this.retryConfig.initialDelayMs;
|
|
|
|
for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
|
|
try {
|
|
return await operation();
|
|
} catch (error) {
|
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
|
|
// Check if error is retryable
|
|
const statusCode = (error as any)?.statusCode || (error as any)?.response?.status;
|
|
const isRetryable = statusCode && isRetryableError(statusCode, this.retryConfig.retryableStatusCodes);
|
|
|
|
// Don't retry on last attempt or if error is not retryable
|
|
if (attempt === this.retryConfig.maxRetries || !isRetryable) {
|
|
throw lastError;
|
|
}
|
|
|
|
// Wait before retrying
|
|
await sleep(Math.min(delay, this.retryConfig.maxDelayMs));
|
|
delay *= this.retryConfig.backoffMultiplier;
|
|
}
|
|
}
|
|
|
|
throw lastError || new Error(`${operationName} failed after ${this.retryConfig.maxRetries} retries`);
|
|
}
|
|
|
|
/**
|
|
* Issue credential with retry logic and manifest selection
|
|
*/
|
|
async issueCredential(
|
|
request: VerifiableCredentialRequest & { manifestName?: string }
|
|
): Promise<VerifiableCredentialResponse> {
|
|
const manifestId = this.getManifestId(request.manifestName);
|
|
|
|
// Create a modified request without manifestName
|
|
const { manifestName, ...credentialRequest } = request;
|
|
|
|
// Temporarily set manifest ID for this request
|
|
const originalManifestId = (this as any).config.credentialManifestId;
|
|
(this as any).config.credentialManifestId = manifestId;
|
|
|
|
try {
|
|
return await this.executeWithRetry(
|
|
() => super.issueCredential(credentialRequest),
|
|
'issueCredential'
|
|
);
|
|
} finally {
|
|
// Restore original manifest ID
|
|
(this as any).config.credentialManifestId = originalManifestId;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get issuance status with retry logic
|
|
*/
|
|
async getIssuanceStatus(requestId: string): Promise<VerifiableCredentialStatus> {
|
|
return this.executeWithRetry(
|
|
() => super.getIssuanceStatus(requestId),
|
|
'getIssuanceStatus'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Verify credential with retry logic
|
|
*/
|
|
async verifyCredential(credential: VerifiedCredential): Promise<boolean> {
|
|
return this.executeWithRetry(
|
|
() => super.verifyCredential(credential),
|
|
'verifyCredential'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create presentation request with retry logic and manifest selection
|
|
*/
|
|
async createPresentationRequest(
|
|
manifestName?: string,
|
|
callbackUrl?: string
|
|
): Promise<VerifiableCredentialResponse> {
|
|
const manifestId = this.getManifestId(manifestName);
|
|
return this.executeWithRetry(
|
|
() => super.createPresentationRequest(manifestId, callbackUrl),
|
|
'createPresentationRequest'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Register a new manifest
|
|
*/
|
|
registerManifest(name: string, manifestId: string): void {
|
|
this.manifests[name] = manifestId;
|
|
}
|
|
|
|
/**
|
|
* Get all registered manifests
|
|
*/
|
|
getManifests(): Record<string, string> {
|
|
return { ...this.manifests };
|
|
}
|
|
}
|
|
|