- 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
196 lines
5.6 KiB
TypeScript
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,
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|