Files
the_order/services/identity/src/batch-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

196 lines
5.6 KiB
TypeScript

/**
* Batch credential issuance endpoint
*/
import { FastifyInstance } from 'fastify';
import { randomUUID } from 'crypto';
import { KMSClient } from '@the-order/crypto';
import { createVerifiableCredential } from '@the-order/database';
import { logCredentialAction } from '@the-order/database';
import { getEnv, authenticateJWT, requireRole } from '@the-order/shared';
export interface BatchIssuanceRequest {
credentials: Array<{
subject: string;
credentialSubject: Record<string, unknown>;
expirationDate?: string;
}>;
}
export interface BatchIssuanceResponse {
jobId: string;
total: number;
accepted: number;
results: Array<{
index: number;
credentialId?: string;
error?: string;
}>;
}
/**
* Register batch issuance endpoint
*/
export async function registerBatchIssuance(
server: FastifyInstance,
kmsClient: KMSClient
): Promise<void> {
const env = getEnv();
const issuerDid = env.VC_ISSUER_DID || (env.VC_ISSUER_DOMAIN ? `did:web:${env.VC_ISSUER_DOMAIN}` : undefined);
server.post(
'/vc/issue/batch',
{
preHandler: [authenticateJWT, requireRole('admin', 'issuer')],
schema: {
body: {
type: 'object',
required: ['credentials'],
properties: {
credentials: {
type: 'array',
minItems: 1,
maxItems: 100, // Limit batch size
items: {
type: 'object',
required: ['subject', 'credentialSubject'],
properties: {
subject: { type: 'string' },
credentialSubject: { type: 'object' },
expirationDate: { type: 'string', format: 'date-time' },
},
},
},
},
},
description: 'Batch issue verifiable credentials',
tags: ['credentials'],
response: {
200: {
type: 'object',
properties: {
jobId: { type: 'string' },
total: { type: 'number' },
accepted: { type: 'number' },
results: {
type: 'array',
items: {
type: 'object',
properties: {
index: { type: 'number' },
credentialId: { type: 'string' },
error: { type: 'string' },
},
},
},
},
},
},
},
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async (request: any, reply: any) => {
if (!issuerDid) {
return reply.code(500).send({
error: {
code: 'CONFIGURATION_ERROR',
message: 'VC_ISSUER_DID or VC_ISSUER_DOMAIN must be configured',
},
});
}
const { credentials } = request.body;
const jobId = randomUUID();
const results: BatchIssuanceResponse['results'] = [];
let accepted = 0;
// Get user ID for audit logging
const user = (request as any).user;
const userId = user?.id || user?.sub || null;
// Process each credential
for (let i = 0; i < credentials.length; i++) {
const cred = credentials[i]!;
try {
const credentialId = randomUUID();
const issuanceDate = new Date();
const expirationDate = cred.expirationDate ? new Date(cred.expirationDate) : undefined;
// Create credential data
const credentialData = {
id: credentialId,
type: ['VerifiableCredential', 'IdentityCredential'],
issuer: issuerDid,
subject: cred.subject,
credentialSubject: cred.credentialSubject,
issuanceDate: issuanceDate.toISOString(),
expirationDate: expirationDate?.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'),
};
const credential = {
...credentialData,
proof,
};
// Save to database
await createVerifiableCredential({
credential_id: credentialId,
issuer_did: issuerDid,
subject_did: cred.subject,
credential_type: credential.type,
credential_subject: cred.credentialSubject,
issuance_date: issuanceDate,
expiration_date: expirationDate,
proof,
});
// Log audit action
await logCredentialAction({
credential_id: credentialId,
issuer_did: issuerDid,
subject_did: cred.subject,
credential_type: credential.type,
action: 'issued',
performed_by: userId || undefined,
metadata: { batchJobId: jobId, batchIndex: i },
ip_address: request.ip,
user_agent: request.headers['user-agent'],
});
results.push({
index: i,
credentialId,
});
accepted++;
} catch (error) {
results.push({
index: i,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
return reply.send({
jobId,
total: credentials.length,
accepted,
results,
});
}
);
}