- 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
188 lines
6.1 KiB
TypeScript
188 lines
6.1 KiB
TypeScript
/**
|
|
* Automated credential renewal system
|
|
* Uses background job queue to scan for expiring credentials and issue renewals
|
|
*/
|
|
|
|
import { getExpiringCredentials, createVerifiableCredential, revokeCredential } from '@the-order/database';
|
|
import { getJobQueue } from '@the-order/jobs';
|
|
import { getEventBus, CredentialEvents } from '@the-order/events';
|
|
import { KMSClient } from '@the-order/crypto';
|
|
import { randomUUID } from 'crypto';
|
|
|
|
export interface RenewalJobData {
|
|
credentialId: string;
|
|
subjectDid: string;
|
|
issuerDid: string;
|
|
credentialType: string[];
|
|
credentialSubject: unknown;
|
|
expirationDate: Date;
|
|
}
|
|
|
|
/**
|
|
* Initialize credential renewal system
|
|
*/
|
|
export async function initializeCredentialRenewal(kmsClient: KMSClient): Promise<void> {
|
|
const jobQueue = getJobQueue();
|
|
const eventBus = getEventBus();
|
|
|
|
// Create renewal queue
|
|
const renewalQueue = jobQueue.createQueue<RenewalJobData>('credential-renewal');
|
|
|
|
// Create worker for renewal jobs
|
|
jobQueue.createWorker<RenewalJobData>(
|
|
'credential-renewal',
|
|
async (job) => {
|
|
const { credentialId, subjectDid, issuerDid, credentialType, credentialSubject, expirationDate } = job.data;
|
|
|
|
try {
|
|
// Issue new credential with extended expiration
|
|
const newExpirationDate = new Date(expirationDate);
|
|
newExpirationDate.setFullYear(newExpirationDate.getFullYear() + 1); // Extend by 1 year
|
|
|
|
const newCredentialId = randomUUID();
|
|
const issuanceDate = new Date();
|
|
|
|
// Create credential data
|
|
const credentialData = {
|
|
id: newCredentialId,
|
|
type: credentialType,
|
|
issuer: issuerDid,
|
|
subject: subjectDid,
|
|
credentialSubject,
|
|
issuanceDate: issuanceDate.toISOString(),
|
|
expirationDate: newExpirationDate.toISOString(),
|
|
};
|
|
|
|
// Sign credential with KMS
|
|
const credentialJson = JSON.stringify(credentialData);
|
|
const signature = await kmsClient.sign(Buffer.from(credentialJson));
|
|
|
|
// Create proof
|
|
const proof = {
|
|
type: 'KmsSignature2024',
|
|
created: issuanceDate.toISOString(),
|
|
proofPurpose: 'assertionMethod',
|
|
verificationMethod: `${issuerDid}#kms-key`,
|
|
jws: signature.toString('base64'),
|
|
};
|
|
|
|
// Save new credential
|
|
await createVerifiableCredential({
|
|
credential_id: newCredentialId,
|
|
issuer_did: issuerDid,
|
|
subject_did: subjectDid,
|
|
credential_type: credentialType,
|
|
credential_subject: credentialSubject as Record<string, unknown>,
|
|
issuance_date: issuanceDate,
|
|
expiration_date: newExpirationDate,
|
|
proof,
|
|
});
|
|
|
|
// Revoke old credential
|
|
await revokeCredential({
|
|
credential_id: credentialId,
|
|
issuer_did: issuerDid,
|
|
revocation_reason: 'Renewed - replaced by new credential',
|
|
});
|
|
|
|
// Publish renewal event
|
|
await eventBus.publish(CredentialEvents.RENEWED, {
|
|
oldCredentialId: credentialId,
|
|
newCredentialId,
|
|
subjectDid,
|
|
renewedAt: issuanceDate.toISOString(),
|
|
});
|
|
|
|
return { success: true, newCredentialId };
|
|
} catch (error) {
|
|
console.error(`Failed to renew credential ${credentialId}:`, error);
|
|
throw error;
|
|
}
|
|
},
|
|
{ concurrency: 5 }
|
|
);
|
|
|
|
// Create worker for expiration scanner
|
|
jobQueue.createWorker<{ daysAhead: number }>(
|
|
'credential-expiration-scanner',
|
|
async (job) => {
|
|
const { daysAhead } = job.data;
|
|
|
|
// Get expiring credentials
|
|
const expiringCredentials = await getExpiringCredentials(daysAhead, 1000);
|
|
|
|
// Publish expiring event for each
|
|
for (const cred of expiringCredentials) {
|
|
await eventBus.publish(CredentialEvents.EXPIRING, {
|
|
credentialId: cred.credential_id,
|
|
subjectDid: cred.subject_did,
|
|
expirationDate: cred.expiration_date!.toISOString(),
|
|
daysUntilExpiration: Math.ceil(
|
|
(cred.expiration_date!.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
|
|
),
|
|
});
|
|
|
|
// For credentials expiring in 30 days or less, queue for renewal
|
|
const daysUntilExpiration = Math.ceil(
|
|
(cred.expiration_date!.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
|
|
);
|
|
|
|
if (daysUntilExpiration <= 30) {
|
|
// Queue renewal job with credential data
|
|
await renewalQueue.add('default', {
|
|
credentialId: cred.credential_id,
|
|
subjectDid: cred.subject_did,
|
|
issuerDid: cred.issuer_did,
|
|
credentialType: cred.credential_type,
|
|
credentialSubject: cred.credential_subject as Record<string, unknown>,
|
|
expirationDate: cred.expiration_date!,
|
|
} as RenewalJobData);
|
|
}
|
|
}
|
|
|
|
return { scanned: expiringCredentials.length };
|
|
}
|
|
);
|
|
|
|
// Schedule recurring job to scan for expiring credentials daily at 2 AM
|
|
await jobQueue.addRecurringJob<{ daysAhead: number }>(
|
|
'credential-expiration-scanner',
|
|
{ daysAhead: 90 }, // Check credentials expiring in next 90 days
|
|
'0 2 * * *' // Run daily at 2 AM
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Manually trigger renewal for a specific credential
|
|
*/
|
|
export async function triggerRenewal(
|
|
credentialId: string,
|
|
_kmsClient: KMSClient
|
|
): Promise<void> {
|
|
const jobQueue = getJobQueue();
|
|
const renewalQueue = jobQueue.createQueue<RenewalJobData>('credential-renewal');
|
|
|
|
// Get credential details
|
|
const { getVerifiableCredentialById } = await import('@the-order/database');
|
|
const credential = await getVerifiableCredentialById(credentialId);
|
|
|
|
if (!credential) {
|
|
throw new Error(`Credential ${credentialId} not found`);
|
|
}
|
|
|
|
if (!credential.expiration_date) {
|
|
throw new Error(`Credential ${credentialId} has no expiration date`);
|
|
}
|
|
|
|
// Queue renewal job
|
|
await renewalQueue.add('default', {
|
|
credentialId: credential.credential_id,
|
|
subjectDid: credential.subject_did,
|
|
issuerDid: credential.issuer_did,
|
|
credentialType: credential.credential_type,
|
|
credentialSubject: credential.credential_subject as Record<string, unknown>,
|
|
expirationDate: credential.expiration_date,
|
|
} as RenewalJobData);
|
|
}
|
|
|