- 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
240 lines
7.8 KiB
TypeScript
240 lines
7.8 KiB
TypeScript
/**
|
|
* Scheduled credential issuance
|
|
* Cron-based jobs for renewal, event-driven issuance, batch operations, expiration detection
|
|
*/
|
|
|
|
import { getJobQueue } from '@the-order/jobs';
|
|
import { getExpiringCredentials, createVerifiableCredential, revokeCredential } from '@the-order/database';
|
|
import { getEventBus, CredentialEvents } from '@the-order/events';
|
|
import { KMSClient } from '@the-order/crypto';
|
|
import { getEnv } from '@the-order/shared';
|
|
import { randomUUID } from 'crypto';
|
|
|
|
export interface ScheduledIssuanceConfig {
|
|
kmsClient: KMSClient;
|
|
enableExpirationDetection?: boolean;
|
|
enableBatchRenewal?: boolean;
|
|
enableScheduledIssuance?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Initialize scheduled credential issuance
|
|
*/
|
|
export async function initializeScheduledIssuance(
|
|
config: ScheduledIssuanceConfig
|
|
): Promise<void> {
|
|
const jobQueue = getJobQueue();
|
|
const eventBus = getEventBus();
|
|
const env = getEnv();
|
|
const issuerDid = env.VC_ISSUER_DID || (env.VC_ISSUER_DOMAIN ? `did:web:${env.VC_ISSUER_DOMAIN}` : undefined);
|
|
|
|
if (!issuerDid) {
|
|
throw new Error('VC_ISSUER_DID or VC_ISSUER_DOMAIN must be configured');
|
|
}
|
|
|
|
// Create scheduled issuance queue
|
|
const scheduledQueue = jobQueue.createQueue<{
|
|
credentialType: string[];
|
|
subjectDid: string;
|
|
credentialSubject: Record<string, unknown>;
|
|
scheduledDate: Date;
|
|
}>('scheduled-issuance');
|
|
|
|
// Create worker for scheduled issuance
|
|
jobQueue.createWorker(
|
|
'scheduled-issuance',
|
|
async (job) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const jobData = job.data as any;
|
|
const { credentialType, subjectDid, credentialSubject, scheduledDate } = jobData;
|
|
const scheduledDateObj = scheduledDate instanceof Date ? scheduledDate : new Date(scheduledDate as string);
|
|
|
|
// Check if it's time to issue
|
|
if (new Date() < scheduledDateObj) {
|
|
// Reschedule for later
|
|
await scheduledQueue.add('default' as any, jobData, {
|
|
delay: scheduledDateObj.getTime() - Date.now(),
|
|
});
|
|
return { rescheduled: true };
|
|
}
|
|
|
|
// Issue credential
|
|
const credentialId = randomUUID();
|
|
const issuanceDate = new Date();
|
|
|
|
const credentialData = {
|
|
id: credentialId,
|
|
type: credentialType,
|
|
issuer: issuerDid,
|
|
subject: subjectDid,
|
|
credentialSubject,
|
|
issuanceDate: issuanceDate.toISOString(),
|
|
};
|
|
|
|
const credentialJson = JSON.stringify(credentialData);
|
|
const signature = await config.kmsClient.sign(Buffer.from(credentialJson));
|
|
|
|
const proof = {
|
|
type: 'KmsSignature2024',
|
|
created: issuanceDate.toISOString(),
|
|
proofPurpose: 'assertionMethod',
|
|
verificationMethod: `${issuerDid}#kms-key`,
|
|
jws: signature.toString('base64'),
|
|
};
|
|
|
|
await createVerifiableCredential({
|
|
credential_id: credentialId,
|
|
issuer_did: issuerDid,
|
|
subject_did: subjectDid as string,
|
|
credential_type: credentialType as string[],
|
|
credential_subject: credentialSubject as Record<string, unknown>,
|
|
issuance_date: issuanceDate,
|
|
expiration_date: undefined,
|
|
proof,
|
|
});
|
|
|
|
await eventBus.publish(CredentialEvents.ISSUED, {
|
|
subjectDid,
|
|
credentialType,
|
|
credentialId,
|
|
issuedAt: issuanceDate.toISOString(),
|
|
});
|
|
|
|
return { credentialId, issued: true };
|
|
}
|
|
);
|
|
|
|
// Expiration detection job (runs daily at 1 AM)
|
|
if (config.enableExpirationDetection) {
|
|
const expirationQueue = jobQueue.createQueue<{ daysAhead: number }>('expiration-detection');
|
|
await expirationQueue.add('default' as any, { daysAhead: 90 }, {
|
|
repeat: {
|
|
pattern: '0 1 * * *', // Daily at 1 AM
|
|
},
|
|
});
|
|
|
|
jobQueue.createWorker('expiration-detection', async (job) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const { daysAhead } = job.data as any;
|
|
const expiring = await getExpiringCredentials(daysAhead as number, 1000);
|
|
|
|
for (const cred of expiring) {
|
|
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)
|
|
),
|
|
});
|
|
}
|
|
|
|
return { detected: expiring.length };
|
|
});
|
|
}
|
|
|
|
// Batch renewal job (runs weekly on Sunday at 2 AM)
|
|
if (config.enableBatchRenewal) {
|
|
const batchRenewalQueue = jobQueue.createQueue<{ daysAhead: number }>('batch-renewal');
|
|
await batchRenewalQueue.add('default' as any, { daysAhead: 30 }, {
|
|
repeat: {
|
|
pattern: '0 2 * * 0', // Weekly on Sunday at 2 AM
|
|
},
|
|
});
|
|
|
|
jobQueue.createWorker('batch-renewal', async (job) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const { daysAhead } = job.data as any;
|
|
const expiring = await getExpiringCredentials(daysAhead as number, 100);
|
|
|
|
let renewed = 0;
|
|
for (const cred of expiring) {
|
|
try {
|
|
// Issue new credential
|
|
const newCredentialId = randomUUID();
|
|
const issuanceDate = new Date();
|
|
const newExpirationDate = new Date(cred.expiration_date);
|
|
newExpirationDate.setFullYear(newExpirationDate.getFullYear() + 1);
|
|
|
|
const credentialData = {
|
|
id: newCredentialId,
|
|
type: cred.credential_type,
|
|
issuer: cred.issuer_did,
|
|
subject: cred.subject_did,
|
|
credentialSubject: cred.credential_subject as Record<string, unknown>,
|
|
issuanceDate: issuanceDate.toISOString(),
|
|
expirationDate: newExpirationDate.toISOString(),
|
|
};
|
|
|
|
const credentialJson = JSON.stringify(credentialData);
|
|
const signature = await config.kmsClient.sign(Buffer.from(credentialJson));
|
|
|
|
const proof = {
|
|
type: 'KmsSignature2024',
|
|
created: issuanceDate.toISOString(),
|
|
proofPurpose: 'assertionMethod',
|
|
verificationMethod: `${cred.issuer_did}#kms-key`,
|
|
jws: signature.toString('base64'),
|
|
};
|
|
|
|
await createVerifiableCredential({
|
|
credential_id: newCredentialId,
|
|
issuer_did: cred.issuer_did,
|
|
subject_did: cred.subject_did,
|
|
credential_type: cred.credential_type,
|
|
credential_subject: cred.credential_subject as Record<string, unknown>,
|
|
issuance_date: issuanceDate,
|
|
expiration_date: newExpirationDate,
|
|
proof,
|
|
});
|
|
|
|
// Revoke old credential
|
|
await revokeCredential({
|
|
credential_id: cred.credential_id,
|
|
issuer_did: cred.issuer_did,
|
|
revocation_reason: 'Renewed via batch renewal',
|
|
});
|
|
|
|
await eventBus.publish(CredentialEvents.RENEWED, {
|
|
oldCredentialId: cred.credential_id,
|
|
newCredentialId,
|
|
subjectDid: cred.subject_did,
|
|
renewedAt: issuanceDate.toISOString(),
|
|
});
|
|
|
|
renewed++;
|
|
} catch (error) {
|
|
console.error(`Failed to renew credential ${cred.credential_id}:`, error);
|
|
}
|
|
}
|
|
|
|
return { renewed, total: expiring.length };
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule credential issuance for a future date
|
|
*/
|
|
export async function scheduleCredentialIssuance(params: {
|
|
credentialType: string[];
|
|
subjectDid: string;
|
|
credentialSubject: Record<string, unknown>;
|
|
scheduledDate: Date;
|
|
}): Promise<string> {
|
|
const jobQueue = getJobQueue();
|
|
const scheduledQueue = jobQueue.createQueue<typeof params>('scheduled-issuance');
|
|
|
|
const delay = params.scheduledDate.getTime() - Date.now();
|
|
if (delay <= 0) {
|
|
throw new Error('Scheduled date must be in the future');
|
|
}
|
|
|
|
const job = await scheduledQueue.add('default' as any, params, {
|
|
delay,
|
|
});
|
|
|
|
return job.id!;
|
|
}
|
|
|