Files
the_order/services/identity/src/scheduled-issuance.ts
defiQUG 92cc41d26d Add Legal Office seal and complete Azure CDN deployment
- 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
2025-11-12 22:03:42 -08:00

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!;
}