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
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* Batch credential issuance endpoint
|
||||
*/
|
||||
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { KMSClient } from '@the-order/crypto';
|
||||
import { createVerifiableCredential } from '@the-order/database';
|
||||
@@ -62,7 +62,7 @@ export async function registerBatchIssuance(
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
description: 'Batch issue verifiable credentials',
|
||||
tags: ['credentials'],
|
||||
response: {
|
||||
@@ -88,7 +88,8 @@ export async function registerBatchIssuance(
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request: FastifyRequest<{ Body: BatchIssuanceRequest }>, reply: FastifyReply) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async (request: any, reply: any) => {
|
||||
if (!issuerDid) {
|
||||
return reply.code(500).send({
|
||||
error: {
|
||||
|
||||
@@ -34,6 +34,7 @@ export async function initializeCredentialNotifications(): Promise<void> {
|
||||
to: eventData.recipientEmail,
|
||||
type: 'email',
|
||||
subject: CredentialNotificationTemplates.ISSUED.email.subject,
|
||||
message: '', // Message will be generated from template
|
||||
template: CredentialNotificationTemplates.ISSUED.email.template,
|
||||
templateData: {
|
||||
recipientName: eventData.recipientName || 'User',
|
||||
@@ -51,6 +52,7 @@ export async function initializeCredentialNotifications(): Promise<void> {
|
||||
await notificationService.send({
|
||||
to: eventData.recipientPhone,
|
||||
type: 'sms',
|
||||
message: '', // Message will be generated from template
|
||||
template: CredentialNotificationTemplates.ISSUED.sms.template,
|
||||
templateData: {
|
||||
credentialType,
|
||||
@@ -78,6 +80,7 @@ export async function initializeCredentialNotifications(): Promise<void> {
|
||||
to: eventData.recipientEmail,
|
||||
type: 'email',
|
||||
subject: CredentialNotificationTemplates.RENEWED.email.subject,
|
||||
message: '', // Message will be generated from template
|
||||
template: CredentialNotificationTemplates.RENEWED.email.template,
|
||||
templateData: {
|
||||
recipientName: eventData.recipientName || 'User',
|
||||
@@ -108,6 +111,7 @@ export async function initializeCredentialNotifications(): Promise<void> {
|
||||
to: eventData.recipientEmail,
|
||||
type: 'email',
|
||||
subject: CredentialNotificationTemplates.EXPIRING.email.subject,
|
||||
message: '', // Message will be generated from template
|
||||
template: CredentialNotificationTemplates.EXPIRING.email.template,
|
||||
templateData: {
|
||||
recipientName: eventData.recipientName || 'User',
|
||||
@@ -124,6 +128,7 @@ export async function initializeCredentialNotifications(): Promise<void> {
|
||||
await notificationService.send({
|
||||
to: eventData.recipientPhone,
|
||||
type: 'sms',
|
||||
message: '', // Message will be generated from template
|
||||
template: CredentialNotificationTemplates.EXPIRING.sms.template,
|
||||
templateData: {
|
||||
credentialType: 'Verifiable Credential',
|
||||
@@ -151,6 +156,7 @@ export async function initializeCredentialNotifications(): Promise<void> {
|
||||
to: eventData.recipientEmail,
|
||||
type: 'email',
|
||||
subject: CredentialNotificationTemplates.REVOKED.email.subject,
|
||||
message: '', // Message will be generated from template
|
||||
template: CredentialNotificationTemplates.REVOKED.email.template,
|
||||
templateData: {
|
||||
recipientName: eventData.recipientName || 'User',
|
||||
|
||||
@@ -157,7 +157,7 @@ export async function initializeCredentialRenewal(kmsClient: KMSClient): Promise
|
||||
*/
|
||||
export async function triggerRenewal(
|
||||
credentialId: string,
|
||||
kmsClient: KMSClient
|
||||
_kmsClient: KMSClient
|
||||
): Promise<void> {
|
||||
const jobQueue = getJobQueue();
|
||||
const renewalQueue = jobQueue.createQueue<RenewalJobData>('credential-renewal');
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function initializeCredentialRevocation(): Promise<void> {
|
||||
|
||||
// Subscribe to user suspension events
|
||||
await eventBus.subscribe(UserEvents.SUSPENDED, async (data) => {
|
||||
const { userId, did } = data as { userId: string; did?: string };
|
||||
const { userId: _userId, did } = data as { userId: string; did?: string };
|
||||
if (did) {
|
||||
await revokeAllUserCredentials(did, 'User account suspended');
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export async function initializeCredentialRevocation(): Promise<void> {
|
||||
|
||||
// Subscribe to role removal events
|
||||
await eventBus.subscribe('role.removed', async (data) => {
|
||||
const { userId, role, did } = data as { userId: string; role: string; did?: string };
|
||||
const { userId: _userId, role, did } = data as { userId: string; role: string; did?: string };
|
||||
if (did) {
|
||||
await revokeRoleCredentials(did, role, 'Role removed');
|
||||
}
|
||||
|
||||
@@ -7,25 +7,63 @@ import {
|
||||
EntraVerifiedIDClient,
|
||||
VerifiableCredentialRequest,
|
||||
} from '@the-order/auth';
|
||||
import { EnhancedEntraVerifiedIDClient } from '@the-order/auth';
|
||||
import { EIDASToEntraBridge } from '@the-order/auth';
|
||||
import { getEnv } from '@the-order/shared';
|
||||
import { createVerifiableCredential } from '@the-order/database';
|
||||
import {
|
||||
entraApiRequests,
|
||||
entraApiRequestDuration,
|
||||
entraApiErrors,
|
||||
entraCredentialsIssued,
|
||||
entraIssuanceDuration,
|
||||
entraIssuanceRetries,
|
||||
entraCredentialsVerified,
|
||||
entraVerificationDuration,
|
||||
entraActiveRequests,
|
||||
} from '@the-order/monitoring';
|
||||
import { registerEntraRateLimit } from '@the-order/shared';
|
||||
|
||||
/**
|
||||
* Initialize Entra VerifiedID client
|
||||
* Initialize Enhanced Entra VerifiedID client with multi-manifest support
|
||||
*/
|
||||
export function createEntraClient(): EntraVerifiedIDClient | null {
|
||||
export function createEntraClient(): EnhancedEntraVerifiedIDClient | null {
|
||||
const env = getEnv();
|
||||
|
||||
if (!env.ENTRA_TENANT_ID || !env.ENTRA_CLIENT_ID || !env.ENTRA_CLIENT_SECRET) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new EntraVerifiedIDClient({
|
||||
// Parse manifests from environment variable if provided
|
||||
let manifests: Record<string, string> | undefined;
|
||||
if (env.ENTRA_MANIFESTS) {
|
||||
try {
|
||||
manifests = JSON.parse(env.ENTRA_MANIFESTS);
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse ENTRA_MANIFESTS, using default manifest only', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Add default manifest if provided
|
||||
if (env.ENTRA_CREDENTIAL_MANIFEST_ID) {
|
||||
manifests = manifests || {};
|
||||
manifests['default'] = env.ENTRA_CREDENTIAL_MANIFEST_ID;
|
||||
}
|
||||
|
||||
return new EnhancedEntraVerifiedIDClient({
|
||||
tenantId: env.ENTRA_TENANT_ID,
|
||||
clientId: env.ENTRA_CLIENT_ID,
|
||||
clientSecret: env.ENTRA_CLIENT_SECRET,
|
||||
credentialManifestId: env.ENTRA_CREDENTIAL_MANIFEST_ID,
|
||||
manifests,
|
||||
logoUri: env.ENTRA_CREDENTIAL_LOGO_URI,
|
||||
backgroundColor: env.ENTRA_CREDENTIAL_BG_COLOR,
|
||||
textColor: env.ENTRA_CREDENTIAL_TEXT_COLOR,
|
||||
}, {
|
||||
maxRetries: 3,
|
||||
initialDelayMs: 1000,
|
||||
maxDelayMs: 10000,
|
||||
backoffMultiplier: 2,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,7 +107,7 @@ export function createEIDASToEntraBridge(): EIDASToEntraBridge | null {
|
||||
/**
|
||||
* Register Entra VerifiedID routes
|
||||
*/
|
||||
export async function registerEntraRoutes(server: FastifyInstance<any, any, any, any, any>): Promise<void> {
|
||||
export async function registerEntraRoutes(server: FastifyInstance): Promise<void> {
|
||||
const entraClient = createEntraClient();
|
||||
const eidasBridge = createEIDASToEntraBridge();
|
||||
|
||||
@@ -78,6 +116,9 @@ export async function registerEntraRoutes(server: FastifyInstance<any, any, any,
|
||||
return;
|
||||
}
|
||||
|
||||
// Register Entra-specific rate limiting
|
||||
await registerEntraRateLimit(server);
|
||||
|
||||
// Issue credential via Entra VerifiedID
|
||||
server.post(
|
||||
'/vc/issue/entra',
|
||||
@@ -125,17 +166,41 @@ export async function registerEntraRoutes(server: FastifyInstance<any, any, any,
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const body = request.body as VerifiableCredentialRequest;
|
||||
const body = request.body as VerifiableCredentialRequest & { manifestName?: string };
|
||||
const startTime = Date.now();
|
||||
entraActiveRequests.inc({ operation: 'issueCredential' });
|
||||
|
||||
try {
|
||||
entraApiRequests.inc({ operation: 'issueCredential', status: 'attempt' });
|
||||
|
||||
const credentialResponse = await entraClient.issueCredential(body);
|
||||
|
||||
// Record success metrics
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
entraIssuanceDuration.observe({ manifest_name: body.manifestName || 'default' }, duration);
|
||||
entraApiRequestDuration.observe({ operation: 'issueCredential' }, duration);
|
||||
entraCredentialsIssued.inc({ manifest_name: body.manifestName || 'default', status: 'success' });
|
||||
entraApiRequests.inc({ operation: 'issueCredential', status: 'success' });
|
||||
|
||||
return reply.status(200).send(credentialResponse);
|
||||
} catch (error) {
|
||||
// Record error metrics
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
entraApiRequestDuration.observe({ operation: 'issueCredential' }, duration);
|
||||
entraApiErrors.inc({
|
||||
operation: 'issueCredential',
|
||||
error_type: error instanceof Error ? error.constructor.name : 'Unknown',
|
||||
status_code: (error as any)?.statusCode || 0,
|
||||
});
|
||||
entraCredentialsIssued.inc({ manifest_name: body.manifestName || 'default', status: 'error' });
|
||||
entraApiRequests.inc({ operation: 'issueCredential', status: 'error' });
|
||||
|
||||
return reply.status(500).send({
|
||||
error: 'Failed to issue credential',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
} finally {
|
||||
entraActiveRequests.dec({ operation: 'issueCredential' });
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -169,18 +234,42 @@ export async function registerEntraRoutes(server: FastifyInstance<any, any, any,
|
||||
},
|
||||
async (request, reply) => {
|
||||
const body = request.body as { credential: unknown };
|
||||
const startTime = Date.now();
|
||||
entraActiveRequests.inc({ operation: 'verifyCredential' });
|
||||
|
||||
try {
|
||||
entraApiRequests.inc({ operation: 'verifyCredential', status: 'attempt' });
|
||||
|
||||
const verified = await entraClient.verifyCredential(
|
||||
body.credential as Parameters<typeof entraClient.verifyCredential>[0]
|
||||
);
|
||||
|
||||
// Record success metrics
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
entraVerificationDuration.observe(duration);
|
||||
entraApiRequestDuration.observe({ operation: 'verifyCredential' }, duration);
|
||||
entraCredentialsVerified.inc({ result: verified ? 'verified' : 'not_verified' });
|
||||
entraApiRequests.inc({ operation: 'verifyCredential', status: 'success' });
|
||||
|
||||
return reply.status(200).send({ verified });
|
||||
} catch (error) {
|
||||
// Record error metrics
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
entraApiRequestDuration.observe({ operation: 'verifyCredential' }, duration);
|
||||
entraApiErrors.inc({
|
||||
operation: 'verifyCredential',
|
||||
error_type: error instanceof Error ? error.constructor.name : 'Unknown',
|
||||
status_code: (error as any)?.statusCode || 0,
|
||||
});
|
||||
entraCredentialsVerified.inc({ result: 'error' });
|
||||
entraApiRequests.inc({ operation: 'verifyCredential', status: 'error' });
|
||||
|
||||
return reply.status(500).send({
|
||||
error: 'Failed to verify credential',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
} finally {
|
||||
entraActiveRequests.dec({ operation: 'verifyCredential' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
267
services/identity/src/entra-webhooks.ts
Normal file
267
services/identity/src/entra-webhooks.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* Microsoft Entra VerifiedID webhook/callback handler
|
||||
* Handles status updates from Entra VerifiedID service
|
||||
*/
|
||||
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { getEnv } from '@the-order/shared';
|
||||
import { updateVerifiableCredential, getVerifiableCredentialById } from '@the-order/database';
|
||||
import { getEventBus, CredentialEvents } from '@the-order/events';
|
||||
import {
|
||||
entraWebhooksReceived,
|
||||
entraWebhookProcessingDuration,
|
||||
entraWebhookErrors,
|
||||
} from '@the-order/monitoring';
|
||||
import { createLogger } from '@the-order/shared';
|
||||
|
||||
const logger = createLogger('entra-webhooks');
|
||||
|
||||
export interface EntraWebhookPayload {
|
||||
requestId: string;
|
||||
requestStatus: 'request_created' | 'request_retrieved' | 'issuance_successful' | 'issuance_failed';
|
||||
state?: string;
|
||||
code?: string;
|
||||
error?: {
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
credential?: {
|
||||
id: string;
|
||||
type: string[];
|
||||
issuer: string;
|
||||
issuanceDate: string;
|
||||
expirationDate?: string;
|
||||
credentialSubject: Record<string, unknown>;
|
||||
proof: {
|
||||
type: string;
|
||||
created: string;
|
||||
proofPurpose: string;
|
||||
verificationMethod: string;
|
||||
jws: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate webhook signature (if Entra provides one)
|
||||
* Note: Entra VerifiedID may not sign webhooks, so this is a placeholder
|
||||
*/
|
||||
function validateWebhookSignature(_payload: EntraWebhookPayload, _signature?: string): boolean {
|
||||
// TODO: Implement signature validation if Entra provides webhook signing
|
||||
// For now, we rely on HTTPS and callback URL validation
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Entra webhook payload
|
||||
*/
|
||||
async function processWebhook(payload: EntraWebhookPayload): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
const { requestId, requestStatus, error, credential } = payload;
|
||||
|
||||
try {
|
||||
logger.info('Processing Entra webhook', { requestId, requestStatus });
|
||||
|
||||
// Update metrics
|
||||
entraWebhooksReceived.inc({ event_type: requestStatus, status: 'received' });
|
||||
|
||||
// Find credential in database
|
||||
const dbCredential = await getVerifiableCredentialById(requestId);
|
||||
if (!dbCredential) {
|
||||
logger.warn('Credential not found in database', { requestId });
|
||||
entraWebhookErrors.inc({ event_type: requestStatus, error_type: 'not_found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Update credential status based on webhook
|
||||
if (requestStatus === 'issuance_successful' && credential) {
|
||||
// Update credential with full credential data
|
||||
await updateVerifiableCredential(requestId, {
|
||||
status: 'issued',
|
||||
credential_data: credential,
|
||||
issued_at: new Date(credential.issuanceDate),
|
||||
expires_at: credential.expirationDate ? new Date(credential.expirationDate) : undefined,
|
||||
});
|
||||
|
||||
// Publish credential issued event
|
||||
await getEventBus().publish(CredentialEvents.ISSUED, {
|
||||
credentialId: requestId,
|
||||
issuerDid: credential.issuer,
|
||||
subjectDid: dbCredential.subject_did,
|
||||
credentialType: credential.type,
|
||||
issuedAt: credential.issuanceDate,
|
||||
});
|
||||
|
||||
logger.info('Credential issued successfully', { requestId });
|
||||
entraWebhooksReceived.inc({ event_type: requestStatus, status: 'processed' });
|
||||
} else if (requestStatus === 'issuance_failed') {
|
||||
// Update credential status to failed
|
||||
await updateVerifiableCredential(requestId, {
|
||||
status: 'failed',
|
||||
error: error?.message || 'Issuance failed',
|
||||
});
|
||||
|
||||
// Publish credential issuance failed event
|
||||
await getEventBus().publish('credential.issuance.failed', {
|
||||
credentialId: requestId,
|
||||
error: error?.message || 'Unknown error',
|
||||
});
|
||||
|
||||
logger.error('Credential issuance failed', { requestId, error });
|
||||
entraWebhookErrors.inc({ event_type: requestStatus, error_type: 'issuance_failed' });
|
||||
} else if (requestStatus === 'request_retrieved') {
|
||||
// User has retrieved the issuance request
|
||||
await updateVerifiableCredential(requestId, {
|
||||
status: 'pending',
|
||||
});
|
||||
|
||||
logger.info('Issuance request retrieved by user', { requestId });
|
||||
entraWebhooksReceived.inc({ event_type: requestStatus, status: 'processed' });
|
||||
}
|
||||
|
||||
// Record processing duration
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
entraWebhookProcessingDuration.observe({ event_type: requestStatus }, duration);
|
||||
} catch (error) {
|
||||
logger.error('Error processing Entra webhook', { requestId, error });
|
||||
entraWebhookErrors.inc({ event_type: requestStatus, error_type: 'processing_error' });
|
||||
entraWebhookProcessingDuration.observe({ event_type: requestStatus }, (Date.now() - startTime) / 1000);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Entra webhook routes
|
||||
*/
|
||||
export async function registerEntraWebhookRoutes(server: FastifyInstance): Promise<void> {
|
||||
const env = getEnv();
|
||||
|
||||
// Webhook endpoint for Entra VerifiedID callbacks
|
||||
server.post(
|
||||
'/vc/entra/webhook',
|
||||
{
|
||||
schema: {
|
||||
description: 'Webhook endpoint for Entra VerifiedID status updates',
|
||||
tags: ['credentials', 'entra', 'webhooks'],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['requestId', 'requestStatus'],
|
||||
properties: {
|
||||
requestId: { type: 'string' },
|
||||
requestStatus: {
|
||||
type: 'string',
|
||||
enum: ['request_created', 'request_retrieved', 'issuance_successful', 'issuance_failed'],
|
||||
},
|
||||
state: { type: 'string' },
|
||||
code: { type: 'string' },
|
||||
error: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: { type: 'string' },
|
||||
message: { type: 'string' },
|
||||
},
|
||||
},
|
||||
credential: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
type: { type: 'array', items: { type: 'string' } },
|
||||
issuer: { type: 'string' },
|
||||
issuanceDate: { type: 'string' },
|
||||
expirationDate: { type: 'string' },
|
||||
credentialSubject: { type: 'object' },
|
||||
proof: { type: 'object' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
received: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const payload = request.body as EntraWebhookPayload;
|
||||
|
||||
try {
|
||||
// Optional: Validate webhook signature if provided
|
||||
const signature = request.headers['x-entra-signature'] as string | undefined;
|
||||
if (!validateWebhookSignature(payload, signature)) {
|
||||
logger.warn('Invalid webhook signature', { requestId: payload.requestId });
|
||||
return reply.status(401).send({ error: 'Invalid signature' });
|
||||
}
|
||||
|
||||
// Process webhook asynchronously
|
||||
processWebhook(payload).catch((error) => {
|
||||
logger.error('Async webhook processing failed', { error, requestId: payload.requestId });
|
||||
});
|
||||
|
||||
// Return immediately to acknowledge receipt
|
||||
return reply.status(200).send({ received: true });
|
||||
} catch (error) {
|
||||
logger.error('Error handling Entra webhook', { error });
|
||||
return reply.status(500).send({
|
||||
error: 'Failed to process webhook',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Manual status check endpoint (for polling fallback)
|
||||
server.get(
|
||||
'/vc/entra/status/:requestId',
|
||||
{
|
||||
schema: {
|
||||
description: 'Check Entra credential issuance status',
|
||||
tags: ['credentials', 'entra'],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
requestId: { type: 'string' },
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
requestId: { type: 'string' },
|
||||
status: { type: 'string' },
|
||||
credential: { type: 'object' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const { requestId } = request.params as { requestId: string };
|
||||
|
||||
try {
|
||||
const credential = await getVerifiableCredentialById(requestId);
|
||||
if (!credential) {
|
||||
return reply.status(404).send({ error: 'Credential not found' });
|
||||
}
|
||||
|
||||
return reply.status(200).send({
|
||||
requestId,
|
||||
status: credential.status || 'pending',
|
||||
credential: credential.credential_data || null,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error checking credential status', { error, requestId });
|
||||
return reply.status(500).send({
|
||||
error: 'Failed to check status',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
logger.info('Entra webhook routes registered');
|
||||
}
|
||||
|
||||
@@ -208,8 +208,11 @@ async function issueCredentialOnAppointment(
|
||||
appointedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Extract subjectDid from appointmentData or use userId
|
||||
const subjectDidValue = (appointmentData.subjectDid as string) || `did:key:${userId}`;
|
||||
|
||||
await issueCredential({
|
||||
subjectDid,
|
||||
subjectDid: subjectDidValue,
|
||||
issuerDid,
|
||||
credentialType: ['VerifiableCredential', 'AppointmentCredential', `${role}Credential`],
|
||||
credentialSubject,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { KMSClient } from '@the-order/crypto';
|
||||
import { createBodySchema, authenticateJWT, requireRole, getAuthorizationService } from '@the-order/shared';
|
||||
import { authenticateJWT, requireRole, getAuthorizationService } from '@the-order/shared';
|
||||
import { issueFinancialCredential, getFinancialCredentialTemplate, FinancialRole, FINANCIAL_CREDENTIAL_TYPES } from './financial-credentials';
|
||||
import { logCredentialAction } from '@the-order/database';
|
||||
import { getEnv } from '@the-order/shared';
|
||||
@@ -34,7 +34,7 @@ export async function registerFinancialCredentialRoutes(
|
||||
expirationDate: { type: 'string', format: 'date-time' },
|
||||
additionalClaims: { type: 'object' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
description: 'Issue financial role credential',
|
||||
tags: ['financial-credentials'],
|
||||
},
|
||||
|
||||
@@ -95,6 +95,10 @@ async function initializeServer(): Promise<void> {
|
||||
const { registerEntraRoutes } = await import('./entra-integration');
|
||||
await registerEntraRoutes(server);
|
||||
|
||||
// Register Entra webhook routes
|
||||
const { registerEntraWebhookRoutes } = await import('./entra-webhooks');
|
||||
await registerEntraWebhookRoutes(server);
|
||||
|
||||
// Register batch issuance endpoint
|
||||
const { registerBatchIssuance } = await import('./batch-issuance');
|
||||
await registerBatchIssuance(server, kmsClient);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { getEventBus, AppointmentEvents, CredentialEvents } from '@the-order/events';
|
||||
import { issueJudicialCredential, type JudicialRole } from './judicial-credentials';
|
||||
import { KMSClient } from '@the-order/crypto';
|
||||
import { sendEmail } from '@the-order/notifications';
|
||||
import { getNotificationService } from '@the-order/notifications';
|
||||
|
||||
export interface JudicialAppointmentData {
|
||||
userId: string;
|
||||
@@ -93,10 +93,11 @@ export async function initializeJudicialAppointmentIssuance(
|
||||
|
||||
// Send notification
|
||||
if (appointmentData.recipientEmail) {
|
||||
await sendEmail({
|
||||
const notificationService = getNotificationService();
|
||||
await notificationService.send({
|
||||
to: appointmentData.recipientEmail,
|
||||
subject: 'Judicial Appointment Credential Issued',
|
||||
text: `Dear ${appointmentData.recipientName || 'User'},
|
||||
type: 'email',
|
||||
message: `Dear ${appointmentData.recipientName || 'User'},
|
||||
|
||||
Your judicial appointment credential has been issued.
|
||||
|
||||
@@ -109,6 +110,7 @@ You can view your credential at: ${process.env.CREDENTIALS_URL || 'https://theor
|
||||
|
||||
Best regards,
|
||||
The Order`,
|
||||
subject: 'Judicial Appointment Credential Issued',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { KMSClient } from '@the-order/crypto';
|
||||
import { issueJudicialCredential, getJudicialCredentialTemplate, type JudicialRole } from './judicial-credentials';
|
||||
import { createBodySchema, authenticateJWT, requireRole, getAuthorizationService } from '@the-order/shared';
|
||||
import { authenticateJWT, requireRole, getAuthorizationService } from '@the-order/shared';
|
||||
|
||||
export async function registerJudicialRoutes(
|
||||
server: FastifyInstance,
|
||||
@@ -41,7 +41,7 @@ export async function registerJudicialRoutes(
|
||||
expirationDate: { type: 'string', format: 'date-time' },
|
||||
additionalClaims: { type: 'object' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
description: 'Issue judicial credential',
|
||||
tags: ['judicial'],
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
trackLettersOfCredenceStatus,
|
||||
revokeLettersOfCredence,
|
||||
} from './letters-of-credence';
|
||||
import { createBodySchema, authenticateJWT, requireRole, getAuthorizationService } from '@the-order/shared';
|
||||
import { authenticateJWT, requireRole, getAuthorizationService } from '@the-order/shared';
|
||||
|
||||
export async function registerLettersOfCredenceRoutes(
|
||||
server: FastifyInstance,
|
||||
@@ -35,7 +35,7 @@ export async function registerLettersOfCredenceRoutes(
|
||||
additionalClaims: { type: 'object' },
|
||||
useEntraVerifiedID: { type: 'boolean' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
description: 'Issue Letters of Credence',
|
||||
tags: ['diplomatic'],
|
||||
},
|
||||
@@ -124,7 +124,7 @@ export async function registerLettersOfCredenceRoutes(
|
||||
properties: {
|
||||
reason: { type: 'string' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
description: 'Revoke Letters of Credence',
|
||||
tags: ['diplomatic'],
|
||||
},
|
||||
|
||||
@@ -83,11 +83,13 @@ export async function issueLettersOfCredence(
|
||||
credentialManifestId: env.ENTRA_CREDENTIAL_MANIFEST_ID,
|
||||
});
|
||||
|
||||
const issuanceRequest = await entraClient.createIssuanceRequest({
|
||||
subject: data.recipientDid,
|
||||
credentialSubject,
|
||||
expirationDate: expirationDate.toISOString(),
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const issuanceRequest = await entraClient.issueCredential({
|
||||
claims: credentialSubject as any,
|
||||
subjectDid: data.recipientDid,
|
||||
pin: undefined,
|
||||
callbackUrl: undefined,
|
||||
} as any);
|
||||
|
||||
// Store the issuance request reference
|
||||
credentialSubject.entraIssuanceRequest = issuanceRequest;
|
||||
|
||||
@@ -68,7 +68,13 @@ export function initializeLogicAppsWorkflows(
|
||||
throw new Error('eIDAS Verify and Issue workflow not configured');
|
||||
}
|
||||
|
||||
return eidasClient.triggerEIDASVerification(eidasData);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const eidasDataTyped = eidasData as any;
|
||||
return eidasClient.triggerEIDASVerification(
|
||||
eidasDataTyped.documentId || '',
|
||||
eidasDataTyped.userId || '',
|
||||
eidasDataTyped.eidasProviderUrl || ''
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -81,8 +87,10 @@ export function initializeLogicAppsWorkflows(
|
||||
}
|
||||
|
||||
return appointmentClient.triggerWorkflow({
|
||||
eventType: 'appointmentCredential',
|
||||
data: appointmentData,
|
||||
body: {
|
||||
eventType: 'appointmentCredential',
|
||||
data: appointmentData,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -96,8 +104,10 @@ export function initializeLogicAppsWorkflows(
|
||||
}
|
||||
|
||||
return batchRenewalClient.triggerWorkflow({
|
||||
eventType: 'batchRenewal',
|
||||
data: renewalData,
|
||||
body: {
|
||||
eventType: 'batchRenewal',
|
||||
data: renewalData,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -111,8 +121,10 @@ export function initializeLogicAppsWorkflows(
|
||||
}
|
||||
|
||||
return documentAttestationClient.triggerWorkflow({
|
||||
eventType: 'documentAttestation',
|
||||
data: documentData,
|
||||
body: {
|
||||
eventType: 'documentAttestation',
|
||||
data: documentData,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { getCredentialMetrics, getMetricsDashboard } from './metrics';
|
||||
import { searchAuditLogs, exportAuditLogs } from '@the-order/database';
|
||||
import { authenticateJWT, requireRole, createBodySchema } from '@the-order/shared';
|
||||
import { authenticateJWT, requireRole } from '@the-order/shared';
|
||||
|
||||
export async function registerMetricsRoutes(server: FastifyInstance): Promise<void> {
|
||||
// Get credential metrics
|
||||
@@ -50,7 +50,8 @@ export async function registerMetricsRoutes(server: FastifyInstance): Promise<vo
|
||||
tags: ['metrics'],
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async (_request: any, reply: any) => {
|
||||
const dashboard = await getMetricsDashboard();
|
||||
return reply.send(dashboard);
|
||||
}
|
||||
@@ -62,7 +63,7 @@ export async function registerMetricsRoutes(server: FastifyInstance): Promise<vo
|
||||
{
|
||||
preHandler: [authenticateJWT, requireRole('admin', 'auditor')],
|
||||
schema: {
|
||||
...createBodySchema({
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
credentialId: { type: 'string' },
|
||||
@@ -77,7 +78,7 @@ export async function registerMetricsRoutes(server: FastifyInstance): Promise<vo
|
||||
page: { type: 'number' },
|
||||
pageSize: { type: 'number' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
description: 'Search audit logs',
|
||||
tags: ['metrics'],
|
||||
},
|
||||
@@ -123,7 +124,7 @@ export async function registerMetricsRoutes(server: FastifyInstance): Promise<vo
|
||||
{
|
||||
preHandler: [authenticateJWT, requireRole('admin', 'auditor')],
|
||||
schema: {
|
||||
...createBodySchema({
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
credentialId: { type: 'string' },
|
||||
@@ -135,7 +136,7 @@ export async function registerMetricsRoutes(server: FastifyInstance): Promise<vo
|
||||
endDate: { type: 'string', format: 'date-time' },
|
||||
format: { type: 'string', enum: ['json', 'csv'] },
|
||||
},
|
||||
}),
|
||||
},
|
||||
description: 'Export audit logs',
|
||||
tags: ['metrics'],
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { getAuditStatistics, searchAuditLogs } from '@the-order/database';
|
||||
import { getPool } from '@the-order/database';
|
||||
// import { getPool } from '@the-order/database'; // Not used in this file
|
||||
import { query } from '@the-order/database';
|
||||
|
||||
export interface CredentialMetrics {
|
||||
@@ -123,8 +123,8 @@ async function getIssuanceCount(startDate: Date, endDate: Date): Promise<number>
|
||||
* Note: This requires tracking issuance time in the audit log metadata
|
||||
*/
|
||||
async function getPerformanceMetrics(
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
_startDate: Date,
|
||||
_endDate: Date
|
||||
): Promise<{
|
||||
average: number;
|
||||
p50: number;
|
||||
|
||||
@@ -44,13 +44,16 @@ export async function initializeScheduledIssuance(
|
||||
jobQueue.createWorker(
|
||||
'scheduled-issuance',
|
||||
async (job) => {
|
||||
const { credentialType, subjectDid, credentialSubject, scheduledDate } = job.data;
|
||||
// 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() < scheduledDate) {
|
||||
if (new Date() < scheduledDateObj) {
|
||||
// Reschedule for later
|
||||
await scheduledQueue.add('default' as any, job.data, {
|
||||
delay: scheduledDate.getTime() - Date.now(),
|
||||
await scheduledQueue.add('default' as any, jobData, {
|
||||
delay: scheduledDateObj.getTime() - Date.now(),
|
||||
});
|
||||
return { rescheduled: true };
|
||||
}
|
||||
@@ -82,9 +85,9 @@ export async function initializeScheduledIssuance(
|
||||
await createVerifiableCredential({
|
||||
credential_id: credentialId,
|
||||
issuer_did: issuerDid,
|
||||
subject_did: subjectDid,
|
||||
credential_type: credentialType,
|
||||
credential_subject: credentialSubject,
|
||||
subject_did: subjectDid as string,
|
||||
credential_type: credentialType as string[],
|
||||
credential_subject: credentialSubject as Record<string, unknown>,
|
||||
issuance_date: issuanceDate,
|
||||
expiration_date: undefined,
|
||||
proof,
|
||||
@@ -111,8 +114,9 @@ export async function initializeScheduledIssuance(
|
||||
});
|
||||
|
||||
jobQueue.createWorker('expiration-detection', async (job) => {
|
||||
const { daysAhead } = job.data;
|
||||
const expiring = await getExpiringCredentials(daysAhead, 1000);
|
||||
// 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, {
|
||||
@@ -139,8 +143,9 @@ export async function initializeScheduledIssuance(
|
||||
});
|
||||
|
||||
jobQueue.createWorker('batch-renewal', async (job) => {
|
||||
const { daysAhead } = job.data;
|
||||
const expiring = await getExpiringCredentials(daysAhead, 100);
|
||||
// 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) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
createTemplateVersion,
|
||||
renderCredentialFromTemplate,
|
||||
} from '@the-order/database';
|
||||
import { createBodySchema, authenticateJWT, requireRole } from '@the-order/shared';
|
||||
import { authenticateJWT, requireRole } from '@the-order/shared';
|
||||
|
||||
export async function registerTemplateRoutes(server: FastifyInstance): Promise<void> {
|
||||
// Create template
|
||||
@@ -21,7 +21,7 @@ export async function registerTemplateRoutes(server: FastifyInstance): Promise<v
|
||||
{
|
||||
preHandler: [authenticateJWT, requireRole('admin', 'issuer')],
|
||||
schema: {
|
||||
...createBodySchema({
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['name', 'credential_type', 'template_data'],
|
||||
properties: {
|
||||
@@ -32,7 +32,7 @@ export async function registerTemplateRoutes(server: FastifyInstance): Promise<v
|
||||
version: { type: 'number' },
|
||||
is_active: { type: 'boolean' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
description: 'Create a credential template',
|
||||
tags: ['templates'],
|
||||
},
|
||||
@@ -172,14 +172,14 @@ export async function registerTemplateRoutes(server: FastifyInstance): Promise<v
|
||||
id: { type: 'string' },
|
||||
},
|
||||
},
|
||||
...createBodySchema({
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
description: { type: 'string' },
|
||||
template_data: { type: 'object' },
|
||||
is_active: { type: 'boolean' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
description: 'Update credential template',
|
||||
tags: ['templates'],
|
||||
},
|
||||
@@ -214,13 +214,13 @@ export async function registerTemplateRoutes(server: FastifyInstance): Promise<v
|
||||
id: { type: 'string' },
|
||||
},
|
||||
},
|
||||
...createBodySchema({
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
template_data: { type: 'object' },
|
||||
description: { type: 'string' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
description: 'Create new version of credential template',
|
||||
tags: ['templates'],
|
||||
},
|
||||
@@ -249,13 +249,13 @@ export async function registerTemplateRoutes(server: FastifyInstance): Promise<v
|
||||
id: { type: 'string' },
|
||||
},
|
||||
},
|
||||
...createBodySchema({
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['variables'],
|
||||
properties: {
|
||||
variables: { type: 'object' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
description: 'Render credential template with variables',
|
||||
tags: ['templates'],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user